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,61 +95,55 @@
</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> -->
<div class=" flex w-full space-x-2"> {#each notes[timeRange] as note, idx (note.id)}
<div class="flex flex-1"> <div class="mb-5 gap-2 grid @lg:grid-cols-2 @2xl:grid-cols-3">
<div class=" self-center ml-1 mr-3"> <div
<Search className="size-3.5" /> 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> >
<input <div class=" flex flex-1 space-x-4 cursor-pointer w-full">
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent" <a href={`/notes/${note.id}`} class="w-full -translate-y-0.5">
bind:value={query} <div class=" flex-1 flex items-center gap-2 self-center">
placeholder={$i18n.t('Search Notes')} <div class=" font-semibold line-clamp-1 capitalize">{note.title}</div>
/> </div>
</div>
</div>
</div>
<div class="mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3"> <div class=" text-xs text-gray-500 dark:text-gray-500 line-clamp-2 pb-2">
{#each notes as note} {#if note.data?.content}
<div {note.data?.content}
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" {:else}
> {$i18n.t('No content')}
<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> {/if}
<a href={`/notes/${note.id}`}> </div>
<div class=" flex-1 flex items-center gap-2 self-center">
<div class=" font-semibold line-clamp-1 capitalize">{note.title}</div>
</div>
<div class=" text-xs px-0.5"> <div class=" text-xs px-0.5 w-full flex justify-between items-center">
<Tooltip <div>
content={note?.user?.email ?? $i18n.t('Deleted User')} {dayjs(note.updated_at / 1000000).fromNow()}
className="flex shrink-0"
placement="top-start"
>
<div class="shrink-0 text-gray-500">
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
note?.user?.name ?? note?.user?.email ?? $i18n.t('Deleted User')
)
})}
</div> </div>
</Tooltip> <Tooltip
</div> content={note?.user?.email ?? $i18n.t('Deleted User')}
</a> className="flex shrink-0"
placement="top-start"
>
<div class="shrink-0 text-gray-500">
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
note?.user?.name ?? note?.user?.email ?? $i18n.t('Deleted User')
)
})}
</div>
</Tooltip>
</div>
</a>
</div>
</div> </div>
</div> </div>
{/each} {/each}
</div> {/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}