feat: note list ui

This commit is contained in:
Timothy Jaeryang Baek 2025-05-03 18:52:13 +04:00
parent 7fee84c06e
commit 7de6112c5b
5 changed files with 113 additions and 60 deletions

View File

@ -6,7 +6,7 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
from pydantic import BaseModel from pydantic import BaseModel
from open_webui.models.users import Users, UserNameResponse from open_webui.models.users import Users, UserResponse
from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
@ -33,9 +33,7 @@ async def get_notes(user=Depends(get_verified_user)):
NoteUserResponse( NoteUserResponse(
**{ **{
**note.model_dump(), **note.model_dump(),
"user": UserNameResponse( "user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()),
**Users.get_user_by_id(note.user_id).model_dump()
),
} }
) )
for note in Notes.get_notes_by_user_id(user.id, "write") for note in Notes.get_notes_by_user_id(user.id, "write")
@ -50,9 +48,7 @@ async def get_note_list(user=Depends(get_verified_user)):
NoteUserResponse( NoteUserResponse(
**{ **{
**note.model_dump(), **note.model_dump(),
"user": UserNameResponse( "user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()),
**Users.get_user_by_id(note.user_id).model_dump()
),
} }
) )
for note in Notes.get_notes_by_user_id(user.id, "read") for note in Notes.get_notes_by_user_id(user.id, "read")

View File

@ -1,8 +1,10 @@
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL } from '$lib/constants';
import { getTimeRange } from '$lib/utils';
type NoteItem = { type NoteItem = {
title: string; title: string;
content: string; data: object;
meta?: null | object;
access_control?: null | object; access_control?: null | object;
}; };
@ -65,7 +67,24 @@ export const getNotes = async (token: string = '') => {
throw error; throw error;
} }
return res; if (!Array.isArray(res)) {
return {}; // or throw new Error("Notes response is not an array")
}
// Build the grouped object
const grouped: Record<string, any[]> = {};
for (const note of res) {
const timeRange = getTimeRange(note.updated_at / 1000000000);
if (!grouped[timeRange]) {
grouped[timeRange] = [];
}
grouped[timeRange].push({
...note,
timeRange
});
}
return grouped;
}; };
export const getNoteById = async (token: string, id: string) => { export const getNoteById = async (token: string, id: string) => {

View File

@ -3,11 +3,32 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import dayjs from '$lib/dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(duration);
dayjs.extend(relativeTime);
async function loadLocale(locales) {
for (const locale of locales) {
try {
dayjs.locale(locale);
break; // Stop after successfully loading the first available locale
} catch (error) {
console.error(`Could not load locale '${locale}':`, error);
}
}
}
// Assuming $i18n.languages is an array of language codes
$: loadLocale($i18n.languages);
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores'; import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
import { getNotes } from '$lib/apis/notes'; import { createNewNote, getNotes } from '$lib/apis/notes';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte'; import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
@ -31,8 +52,24 @@
const init = async () => { const init = async () => {
notes = await getNotes(localStorage.token); notes = await getNotes(localStorage.token);
};
console.log(notes); const createNoteHandler = async () => {
const res = await createNewNote(localStorage.token, {
title: $i18n.t('New Note'),
data: {
content: ''
},
meta: null,
access_control: null
}).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
goto(`/notes/${res.id}`);
}
}; };
onMount(async () => { onMount(async () => {
@ -58,42 +95,35 @@
</div> </div>
</DeleteConfirmDialog> </DeleteConfirmDialog>
{#if notes.length > 0} {#if Object.keys(notes).length > 0}
<div class="flex flex-col gap-1 my-1.5"> {#each Object.keys(notes) as timeRange}
<!-- <div class="flex justify-between items-center"> <div class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium pb-2">
<div class="flex md:self-center text-xl font-medium px-0.5 items-center"> {$i18n.t(timeRange)}
{$i18n.t('Notes')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{notes.length}</span>
</div>
</div> -->
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Notes')}
/>
</div>
</div>
</div> </div>
<div class="mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3"> {#each notes[timeRange] as note, idx (note.id)}
{#each notes as note} <div class="mb-5 gap-2 grid @lg:grid-cols-2 @2xl:grid-cols-3">
<div <div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition" class=" flex space-x-4 cursor-pointer w-full px-4 py-3.5 bg-gray-50 dark:bg-gray-850 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition"
> >
<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> <div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a href={`/notes/${note.id}`}> <a href={`/notes/${note.id}`} class="w-full -translate-y-0.5">
<div class=" flex-1 flex items-center gap-2 self-center"> <div class=" flex-1 flex items-center gap-2 self-center">
<div class=" font-semibold line-clamp-1 capitalize">{note.title}</div> <div class=" font-semibold line-clamp-1 capitalize">{note.title}</div>
</div> </div>
<div class=" text-xs px-0.5"> <div class=" text-xs text-gray-500 dark:text-gray-500 line-clamp-2 pb-2">
{#if note.data?.content}
{note.data?.content}
{:else}
{$i18n.t('No content')}
{/if}
</div>
<div class=" text-xs px-0.5 w-full flex justify-between items-center">
<div>
{dayjs(note.updated_at / 1000000).fromNow()}
</div>
<Tooltip <Tooltip
content={note?.user?.email ?? $i18n.t('Deleted User')} content={note?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0" className="flex shrink-0"
@ -111,8 +141,9 @@
</a> </a>
</div> </div>
</div> </div>
{/each}
</div> </div>
{/each}
{/each}
{:else} {:else}
<div class="w-full h-full flex flex-col items-center justify-center"> <div class="w-full h-full flex flex-col items-center justify-center">
<div class="pb-20 text-center"> <div class="pb-20 text-center">
@ -133,7 +164,9 @@
<button <button
class="cursor-pointer p-2.5 flex rounded-full bg-gray-50 dark:bg-gray-850 hover:bg-gray-100 dark:hover:bg-gray-800 transition shadow-xl" class="cursor-pointer p-2.5 flex rounded-full bg-gray-50 dark:bg-gray-850 hover:bg-gray-100 dark:hover:bg-gray-800 transition shadow-xl"
type="button" type="button"
on:click={async () => {}} on:click={async () => {
createNoteHandler();
}}
> >
<Plus className="size-4.5" strokeWidth="2.5" /> <Plus className="size-4.5" strokeWidth="2.5" />
</button> </button>

View File

@ -85,7 +85,7 @@
</div> </div>
</nav> </nav>
<div class=" pb-1 px-[18px] flex-1 max-h-full overflow-y-auto"> <div class=" pb-1 px-[18px] flex-1 max-h-full overflow-y-auto @container">
<slot /> <slot />
</div> </div>
</div> </div>

View File

@ -0,0 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
{$page.params.id}