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"
|
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}
|
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
|
Notebook
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,42 +2,96 @@
|
||||||
import { notebookStore } from '$lib/stores/notebook.svelte';
|
import { notebookStore } from '$lib/stores/notebook.svelte';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import Textarea from '$lib/components/ui/textarea/textarea.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 { 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 { 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>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col p-4 md:p-6">
|
<div class="flex h-full flex-col">
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<header
|
||||||
<h1 class="text-2xl font-semibold">Notebook</h1>
|
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="flex gap-2">
|
>
|
||||||
{#if notebookStore.isGenerating}
|
<div class="w-10"></div>
|
||||||
<Button variant="destructive" onclick={() => notebookStore.stop()}>
|
<!-- Spacer for centering -->
|
||||||
<Square class="mr-2 h-4 w-4" />
|
<h1 class="text-lg font-semibold">Notebook</h1>
|
||||||
Stop
|
<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>
|
</Button>
|
||||||
{:else}
|
|
||||||
<Button onclick={() => notebookStore.generate()}>
|
<Button variant="ghost" size="icon" onclick={() => (modelInfoOpen = true)}>
|
||||||
<Play class="mr-2 h-4 w-4" />
|
<Info class="h-4 w-4" />
|
||||||
Generate
|
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
|
<ChatMessageStatistics
|
||||||
|
predictedTokens={notebookStore.predictedTokens}
|
||||||
|
predictedMs={notebookStore.predictedMs}
|
||||||
|
promptTokens={notebookStore.promptTokens}
|
||||||
|
promptMs={notebookStore.promptMs}
|
||||||
|
isLive={notebookStore.isGenerating}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-hidden rounded-lg border bg-background shadow-sm">
|
<DialogChatSettings open={settingsOpen} onOpenChange={(open) => (settingsOpen = open)} />
|
||||||
<textarea
|
<DialogModelInformation open={modelInfoOpen} onOpenChange={(open) => (modelInfoOpen = open)} />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,24 @@ export class NotebookStore {
|
||||||
isGenerating = $state(false);
|
isGenerating = $state(false);
|
||||||
abortController: AbortController | null = null;
|
abortController: AbortController | null = null;
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
promptTokens = $state(0);
|
||||||
|
promptMs = $state(0);
|
||||||
|
predictedTokens = $state(0);
|
||||||
|
predictedMs = $state(0);
|
||||||
|
|
||||||
async generate(model?: string) {
|
async generate(model?: string) {
|
||||||
if (this.isGenerating) return;
|
if (this.isGenerating) return;
|
||||||
|
|
||||||
this.isGenerating = true;
|
this.isGenerating = true;
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
|
|
||||||
|
// Reset stats
|
||||||
|
this.promptTokens = 0;
|
||||||
|
this.promptMs = 0;
|
||||||
|
this.predictedTokens = 0;
|
||||||
|
this.predictedMs = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
await ChatService.sendCompletion(
|
await ChatService.sendCompletion(
|
||||||
|
|
@ -23,6 +35,14 @@ export class NotebookStore {
|
||||||
onChunk: (chunk) => {
|
onChunk: (chunk) => {
|
||||||
this.content += 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: () => {
|
onComplete: () => {
|
||||||
this.isGenerating = false;
|
this.isGenerating = false;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue