Refine Notebook UI: improved layout, added stats and model info
This commit is contained in:
parent
6d96745375
commit
3af9b34aa2
Binary file not shown.
|
|
@ -124,7 +124,7 @@
|
|||
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
|
||||
onclick={handleMobileSidebarItemClick}
|
||||
>
|
||||
<span class="i-lucide-book-open h-4 w-4"></span>
|
||||
<span class="i-lucide-file-text h-4 w-4"></span>
|
||||
Notebook
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,42 +2,96 @@
|
|||
import { notebookStore } from '$lib/stores/notebook.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import Textarea from '$lib/components/ui/textarea/textarea.svelte';
|
||||
import { Play, Square } from '@lucide/svelte';
|
||||
import { Play, Square, Settings, Info } from '@lucide/svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import ChatMessageStatistics from '$lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte';
|
||||
import DialogChatSettings from '$lib/components/app/dialogs/DialogChatSettings.svelte';
|
||||
import DialogModelInformation from '$lib/components/app/dialogs/DialogModelInformation.svelte';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
|
||||
let { content } = $state(notebookStore);
|
||||
let settingsOpen = $state(false);
|
||||
let modelInfoOpen = $state(false);
|
||||
|
||||
let inputContent = $state(content);
|
||||
|
||||
// Sync local input with store content
|
||||
$effect(() => {
|
||||
inputContent = notebookStore.content;
|
||||
});
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
notebookStore.content = target.value;
|
||||
}
|
||||
|
||||
async function handleGenerate() {
|
||||
await notebookStore.generate();
|
||||
}
|
||||
|
||||
function handleStop() {
|
||||
notebookStore.stop();
|
||||
}
|
||||
|
||||
let currentModel = $derived(
|
||||
modelsStore.models.find((m) => m.id === config().model) || modelsStore.models[0]
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col p-4 md:p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold">Notebook</h1>
|
||||
<div class="flex gap-2">
|
||||
{#if notebookStore.isGenerating}
|
||||
<Button variant="destructive" onclick={() => notebookStore.stop()}>
|
||||
<Square class="mr-2 h-4 w-4" />
|
||||
Stop
|
||||
<div class="flex h-full flex-col">
|
||||
<header
|
||||
class="flex items-center justify-between border-b border-border/40 bg-background/95 px-6 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
>
|
||||
<div class="w-10"></div>
|
||||
<!-- Spacer for centering -->
|
||||
<h1 class="text-lg font-semibold">Notebook</h1>
|
||||
<Button variant="ghost" size="icon" onclick={() => (settingsOpen = true)}>
|
||||
<Settings class="h-5 w-5" />
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4 md:p-6">
|
||||
<Textarea
|
||||
value={inputContent}
|
||||
oninput={handleInput}
|
||||
class="h-full min-h-[500px] w-full resize-none rounded-xl border-none bg-muted p-4 text-base focus-visible:ring-0 md:p-6"
|
||||
placeholder="Enter your prompt here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border/40 bg-background p-4 md:px-6 md:py-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
onclick={notebookStore.isGenerating ? handleStop : handleGenerate}
|
||||
size="sm"
|
||||
variant={notebookStore.isGenerating ? 'destructive' : 'default'}
|
||||
class="gap-2"
|
||||
>
|
||||
{#if notebookStore.isGenerating}
|
||||
<Square class="h-4 w-4 fill-current" />
|
||||
Stop
|
||||
{:else}
|
||||
<Play class="h-4 w-4 fill-current" />
|
||||
Generate
|
||||
{/if}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button onclick={() => notebookStore.generate()}>
|
||||
<Play class="mr-2 h-4 w-4" />
|
||||
Generate
|
||||
|
||||
<Button variant="ghost" size="icon" onclick={() => (modelInfoOpen = true)}>
|
||||
<Info class="h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ChatMessageStatistics
|
||||
predictedTokens={notebookStore.predictedTokens}
|
||||
predictedMs={notebookStore.predictedMs}
|
||||
promptTokens={notebookStore.promptTokens}
|
||||
promptMs={notebookStore.promptMs}
|
||||
isLive={notebookStore.isGenerating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-hidden rounded-lg border bg-background shadow-sm">
|
||||
<textarea
|
||||
class="h-full w-full resize-none border-0 bg-transparent p-4 font-mono text-sm focus:ring-0 focus-visible:ring-0"
|
||||
placeholder="Enter your text here..."
|
||||
bind:value={notebookStore.content}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-xs text-muted-foreground">
|
||||
<p>
|
||||
Model: {config().model || 'Default'} | Temperature: {config().temperature ?? 0.8} | Max Tokens: {config()
|
||||
.max_tokens ?? -1}
|
||||
</p>
|
||||
</div>
|
||||
<DialogChatSettings open={settingsOpen} onOpenChange={(open) => (settingsOpen = open)} />
|
||||
<DialogModelInformation open={modelInfoOpen} onOpenChange={(open) => (modelInfoOpen = open)} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,24 @@ export class NotebookStore {
|
|||
isGenerating = $state(false);
|
||||
abortController: AbortController | null = null;
|
||||
|
||||
// Statistics
|
||||
promptTokens = $state(0);
|
||||
promptMs = $state(0);
|
||||
predictedTokens = $state(0);
|
||||
predictedMs = $state(0);
|
||||
|
||||
async generate(model?: string) {
|
||||
if (this.isGenerating) return;
|
||||
|
||||
this.isGenerating = true;
|
||||
this.abortController = new AbortController();
|
||||
|
||||
// Reset stats
|
||||
this.promptTokens = 0;
|
||||
this.promptMs = 0;
|
||||
this.predictedTokens = 0;
|
||||
this.predictedMs = 0;
|
||||
|
||||
try {
|
||||
const currentConfig = config();
|
||||
await ChatService.sendCompletion(
|
||||
|
|
@ -23,6 +35,14 @@ export class NotebookStore {
|
|||
onChunk: (chunk) => {
|
||||
this.content += chunk;
|
||||
},
|
||||
onTimings: (timings) => {
|
||||
if (timings) {
|
||||
if (timings.prompt_n) this.promptTokens = timings.prompt_n;
|
||||
if (timings.prompt_ms) this.promptMs = timings.prompt_ms;
|
||||
if (timings.predicted_n) this.predictedTokens = timings.predicted_n;
|
||||
if (timings.predicted_ms) this.predictedMs = timings.predicted_ms;
|
||||
}
|
||||
},
|
||||
onComplete: () => {
|
||||
this.isGenerating = false;
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue