Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show a "nothing indexed" banner when first arriving at index page. #38

Closed
Tracked by #20
ydennisy opened this issue May 29, 2024 · 1 comment · Fixed by #39
Closed
Tracked by #20

Show a "nothing indexed" banner when first arriving at index page. #38

ydennisy opened this issue May 29, 2024 · 1 comment · Fixed by #39
Labels

Comments

@ydennisy
Copy link
Owner

ydennisy commented May 29, 2024

We have a page which allows users to index new URLs, this is in frontend/pages/index.vue, this page contains a table which is hidden until the server returns data.

We poll for this data whilst on the page.

To improve the UX, this we must add:

  • a loading indicator to for this table on first load, to show the table should be there and is loading
  • if nothing is returned, we can show a nothing indexed yet banner
  • we can also show a small icon when we are re-fetching data, i,e whilst the polling network connection is happening.
@ydennisy ydennisy mentioned this issue May 29, 2024
31 tasks
@ydennisy ydennisy added the sweep label May 29, 2024
Copy link
Contributor

sweep-ai bot commented May 29, 2024

🚀 Here's the PR! #39

💎 Sweep Pro: You have unlimited Sweep issues

Actions

  • ↻ Restart Sweep

Step 1: 🔎 Searching

Here are the code search results. I'm now analyzing these search results to write the PR.

Relevant files (click to expand). Mentioned files will always appear here.

<script setup lang="ts">
import { formatDistance } from 'date-fns';
interface IndexFeedResult {
id: string;
created_at: string;
source: string;
status: string;
url: string;
}
definePageMeta({ path: '/index' });
const input = ref('');
const urlsCount = ref(0);
const urls = ref();
const indexingStatusMessage = ref('');
const indexFeedResults = ref<IndexFeedResult[]>([]);
const config = useRuntimeConfig();
const apiBase = config.public.apiBase;
const indexWebPages = async () => {
indexingStatusMessage.value = 'Indexing...';
const token = useSupabaseSession().value?.access_token;
// TODO: handle re-auth
if (!token) return;
const result = await useFetch(`${apiBase}/api/index`, {
method: 'POST',
body: { urls: urls.value },
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (result.status.value == 'error') {
indexingStatusMessage.value = 'Error';
} else {
indexingStatusMessage.value = 'Success';
}
setTimeout(() => {
input.value = '';
urlsCount.value = 0;
urls.value = [];
indexingStatusMessage.value = '';
}, 3000);
};
const formatTimeToHumanFriendly = (time: string) => {
return formatDistance(new Date(time), new Date(), { addSuffix: true });
};
const fetchIndexedPages = async () => {
const token = useSupabaseSession().value?.access_token;
// TODO: handle re-auth
if (!token) return;
const { data } = await useFetch<[IndexFeedResult]>(
`${apiBase}/api/index-feed`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
}
);
if (data.value) {
indexFeedResults.value = data.value;
} else {
indexFeedResults.value = [];
}
};
const fetchIndexedPagesIfTabInFocus = async () => {
if (document.hasFocus()) {
await fetchIndexedPages();
}
};
let indexFeedPollingInterval: NodeJS.Timeout;
onMounted(async () => {
await fetchIndexedPages();
indexFeedPollingInterval = setInterval(fetchIndexedPagesIfTabInFocus, 10000);
});
onUnmounted(() => {
clearInterval(indexFeedPollingInterval);
});
watch(input, (newValue) => {
const inputUrls = newValue.split(/[\n,]+/).filter(Boolean);
urlsCount.value = inputUrls.length;
urls.value = inputUrls;
});
</script>
<template>
<!-- Indexing Input Textarea -->
<div class="relative mt-2">
<textarea
placeholder="Enter the URL(s) you want to index, separated by commas or newlines."
v-model="input"
rows="10"
class="border border-gray-300 rounded pl-4 pb-8 pt-4 pr-20 focus:outline-none focus:border-blue-500 text-gray-700 bg-white shadow-sm w-full"
></textarea>
<!-- Display indexing message -->
<span
v-if="indexingStatusMessage"
:class="{
'text-green-300': indexingStatusMessage === 'Success',
'text-red-300': indexingStatusMessage === 'Error',
}"
class="absolute bottom-2 left-4 bg-white bg-opacity-80 p-1 rounded text-sm text-gray-400"
>
{{ indexingStatusMessage }}
</span>
<!-- Display URLs count -->
<span
v-else
class="absolute bottom-2 left-4 bg-white bg-opacity-80 p-1 rounded text-sm text-gray-400"
>
URLs to index: {{ urlsCount }}
</span>
<button
type="submit"
@click="indexWebPages"
class="absolute bottom-0 right-0 mb-4 mr-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Index
</button>
</div>
<!-- URL Index Feed Table -->
<table
class="rounded-md mt-2 border-collapse table-auto w-full"
v-if="indexFeedResults.length"
>
<thead class="bg-gray-200">
<tr>
<th class="rounded-tl-md py-2 px-4 text-left">URL</th>
<th class="py-2 px-4 text-left">Submitted</th>
<th class="py-2 px-4 text-left">Source</th>
<th class="rounded-tr-md py-2 px-4 text-left">Status</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in indexFeedResults"
:key="index"
class="border-b border-slate-200 hover:bg-gray-100 cursor-pointer"
>
<td class="py-2 px-4 text-xs text-slate-600">
<a :href="item.url" target="_blank" class="text-xs" @click.stop>
{{ item.url }}
</a>
</td>
<td class="py-2 px-4 text-xs text-slate-600">
{{ formatTimeToHumanFriendly(item.created_at) }}
</td>
<td class="py-2 px-4 text-xs text-slate-600">{{ item.source }}</td>
<td class="py-2 px-4 text-xs text-slate-600">{{ item.status }}</td>
</tr>
</tbody>
</table>

<script setup lang="ts">
interface SearchResult {
id: string;
title: string;
url: string;
score: number;
}
const results = ref<SearchResult[]>([]);
const isLoading = ref(false);
const isResultsEmpty = ref(false);
const lastSearchQuery = ref('');
const config = useRuntimeConfig();
const router = useRouter();
const route = useRoute();
const apiBase = config.public.apiBase;
const search = async (query: string) => {
isLoading.value = true;
isResultsEmpty.value = false;
lastSearchQuery.value = query;
const token = useSupabaseSession().value?.access_token;
// TODO: handle re-auth
if (!token) return;
const { data } = await useFetch<SearchResult[]>(`${apiBase}/api/search`, {
method: 'GET',
query: { q: query },
headers: { Authorization: `Bearer ${token}` },
});
// TODO: handle re-auth
if (!data.value) {
results.value = [];
} else {
results.value = data.value;
}
router.push({ path: route.path, query: { q: query } });
if (results.value.length === 0) {
isResultsEmpty.value = true;
}
isLoading.value = false;
};
const navigateToNode = (id: string) => {
router.push({ path: `/node`, query: { id } });
};
onMounted(async () => {
if (route.query.q) {
await search(String(route.query.q));
}
});
</script>
<template>
<!-- Search Bar -->
<SearchBar :is-loading="isLoading" @search="search" />
<!-- Notification Banner -->
<div
v-if="isResultsEmpty"
class="bg-blue-100 border border-blue-300 text-blue-600 mt-2 px-4 py-2 rounded-md relative"
role="alert"
>
<strong class="font-bold text-xs">No results, </strong>
<span class="block sm:inline text-xs"
>found for "{{ lastSearchQuery }}" please try another query or
<NuxtLink class="font-bold text-blue-600 underline" to="/index"
>index</NuxtLink
>
some relevant documents.</span
>
<span class="absolute top-0 bottom-0 right-0 px-4 py-3">
<svg
@click="isResultsEmpty = false"
class="fill-current h-4 w-4 text-blue-600 cursor-pointer"
role="button"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<title>Close</title>
<path
d="M14.348 14.849a1.2 1.2 0 01-1.697 0L10 11.846 7.349 14.849a1.2 1.2 0 11-1.697-1.697L8.303 10 5.652 6.849a1.2 1.2 0 111.697-1.697L10 8.302l2.651-3.15a1.2 1.2 0 011.697 1.697L11.697 10l2.651 2.849a1.2 1.2 0 010 1.697z"
/>
</svg>
</span>
</div>
<!-- Results table -->
<table
class="rounded-md mt-2 border-collapse table-auto w-full"
v-if="results.length"
>
<thead class="bg-gray-200">
<tr>
<th class="rounded-tl-md py-2 px-4 text-left">Title</th>
<th class="rounded-tr-md py-2 px-4 text-left">Score</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in results"
:key="index"
class="border-b border-slate-200 hover:bg-gray-100 cursor-pointer"
@click="navigateToNode(item.id)"
>
<td class="py-2 px-4">
<div style="display: flex; align-items: center">
<span style="margin-right: 10px">➔</span>
<div>
<div class="text-sm text-slate-600">{{ item.title }}</div>
<div>
<a :href="item.url" target="_blank" class="text-xs" @click.stop>
{{ item.url }}
</a>
</div>
</div>
</div>
</td>
<td class="py-2 px-4 text-sm text-slate-600">{{ item.score }}</td>
</tr>
</tbody>
</table>

Step 2: ⌨️ Coding

frontend/pages/index.vue

Add reactive variables for table loading, empty state, and refreshing.
--- 
+++ 
@@ -1,4 +1,7 @@
 const indexFeedResults = ref<IndexFeedResult[]>([]);
+const isTableLoading = ref(true);
+const isTableEmpty = ref(false);
+const isTableRefreshing = ref(false);
 
 const config = useRuntimeConfig();
 const apiBase = config.public.apiBase;

frontend/pages/index.vue

Update `fetchIndexedPages` to handle table loading and empty state.
--- 
+++ 
@@ -13,7 +13,10 @@
   );
   if (data.value) {
     indexFeedResults.value = data.value;
+    isTableEmpty.value = data.value.length === 0;
   } else {
     indexFeedResults.value = [];
+    isTableEmpty.value = true;
   }
+  isTableLoading.value = false;
 };

frontend/pages/index.vue

Update `fetchIndexedPagesIfTabInFocus` to handle table refreshing state.
--- 
+++ 
@@ -1,5 +1,7 @@
 const fetchIndexedPagesIfTabInFocus = async () => {
   if (document.hasFocus()) {
+    isTableRefreshing.value = true;
     await fetchIndexedPages();
+    isTableRefreshing.value = false;
   }
 };

frontend/pages/index.vue

Update the template to display loading indicator, empty state banner, and refreshing icon.
--- 
+++ 
@@ -1,14 +1,39 @@
+<!-- Loading Indicator -->
+<div v-if="isTableLoading" class="mt-4 text-center">
+  <span class="text-gray-500">Loading...</span>
+</div>
+
+<!-- Empty State Banner -->
+<div
+  v-else-if="isTableEmpty"
+  class="bg-blue-100 border border-blue-300 text-blue-600 mt-4 px-4 py-2 rounded-md relative"
+  role="alert"
+>
+  <strong class="font-bold text-xs">Nothing indexed yet.</strong>
+  <span class="block sm:inline text-xs">
+    Please <NuxtLink class="font-bold text-blue-600 underline" to="/index">index</NuxtLink> some relevant documents.
+  </span>
+</div>
+
 <!-- URL Index Feed Table -->
 <table
+  v-else
   class="rounded-md mt-2 border-collapse table-auto w-full"
-  v-if="indexFeedResults.length"
 >
   <thead class="bg-gray-200">
     <tr>
       <th class="rounded-tl-md py-2 px-4 text-left">URL</th>
       <th class="py-2 px-4 text-left">Submitted</th>
       <th class="py-2 px-4 text-left">Source</th>
-      <th class="rounded-tr-md py-2 px-4 text-left">Status</th>
+      <th class="rounded-tr-md py-2 px-4 text-left">
+        Status
+        <span v-if="isTableRefreshing" class="ml-2 text-gray-500">
+          <svg class="animate-spin h-4 w-4 inline-block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+          </svg>
+        </span>
+      </th>
     </tr>
   </thead>
   <tbody>

Step 3: 🔄️ Validating

Your changes have been successfully made to the branch sweep/show_a_nothing_indexed_banner_when_first. I have validated these changes using a syntax checker and a linter.


Tip

To recreate the pull request, edit the issue title or description.

This is an automated message generated by Sweep AI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant