Add .agent/ to gitignore

Fix buttons
Fix model loading with router enabled
remove stats for now
lint
This commit is contained in:
Leszek Hanusz 2026-02-01 12:12:52 +01:00
parent 3af9b34aa2
commit c9f9863268
7 changed files with 187 additions and 112 deletions

1
.gitignore vendored
View File

@ -136,5 +136,6 @@ poetry.toml
# IDE # IDE
/*.code-workspace /*.code-workspace
/.windsurf/ /.windsurf/
/.agent/
# emscripten # emscripten
a.out.* a.out.*

Binary file not shown.

View File

@ -118,17 +118,6 @@
<ChatSidebarActions {handleMobileSidebarItemClick} bind:isSearchModeActive bind:searchQuery /> <ChatSidebarActions {handleMobileSidebarItemClick} bind:isSearchModeActive bind:searchQuery />
</Sidebar.Header> </Sidebar.Header>
<div class="px-4 py-2">
<a
href="#/notebook"
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-file-text h-4 w-4"></span>
Notebook
</a>
</div>
<Sidebar.Group class="mt-4 space-y-2 p-0 px-4"> <Sidebar.Group class="mt-4 space-y-2 p-0 px-4">
{#if (filteredConversations.length > 0 && isSearchModeActive) || !isSearchModeActive} {#if (filteredConversations.length > 0 && isSearchModeActive) || !isSearchModeActive}
<Sidebar.GroupLabel> <Sidebar.GroupLabel>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Search, SquarePen, X } from '@lucide/svelte'; import { NotepadText, Search, SquarePen, X } from '@lucide/svelte';
import { KeyboardShortcutInfo } from '$lib/components/app'; import { KeyboardShortcutInfo } from '$lib/components/app';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
@ -63,6 +63,18 @@
<KeyboardShortcutInfo keys={['shift', 'cmd', 'o']} /> <KeyboardShortcutInfo keys={['shift', 'cmd', 'o']} />
</Button> </Button>
<Button
class="w-full justify-between hover:[&>kbd]:opacity-100"
href="#/notebook"
onclick={handleMobileSidebarItemClick}
variant="ghost"
>
<div class="flex items-center gap-2">
<NotepadText class="h-4 w-4" />
Notebook
</div>
</Button>
<Button <Button
class="w-full justify-between hover:[&>kbd]:opacity-100" class="w-full justify-between hover:[&>kbd]:opacity-100"
onclick={() => { onclick={() => {

View File

@ -2,19 +2,22 @@
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, Settings, Info } from '@lucide/svelte'; import { Play, Square, Settings } 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 DialogChatSettings from '$lib/components/app/dialogs/DialogChatSettings.svelte';
import DialogModelInformation from '$lib/components/app/dialogs/DialogModelInformation.svelte'; import { ModelsSelector } from '$lib/components/app';
import { modelsStore } from '$lib/stores/models.svelte'; import { useModelChangeValidation } from '$lib/hooks/use-model-change-validation.svelte';
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import * as Tooltip from '$lib/components/ui/tooltip';
let { content } = $state(notebookStore); let { content } = $state(notebookStore);
let settingsOpen = $state(false); let settingsOpen = $state(false);
let modelInfoOpen = $state(false);
let inputContent = $state(content); let inputContent = $state(content);
let isRouter = $derived(isRouterMode());
// Sync local input with store content // Sync local input with store content
$effect(() => { $effect(() => {
inputContent = notebookStore.content; inputContent = notebookStore.content;
@ -26,16 +29,70 @@
} }
async function handleGenerate() { async function handleGenerate() {
await notebookStore.generate(); if (notebookModel == null) {
notebookModel = activeModelId;
}
await notebookStore.generate(notebookModel);
} }
function handleStop() { function handleStop() {
notebookStore.stop(); notebookStore.stop();
} }
let currentModel = $derived( let activeModelId = $derived.by(() => {
modelsStore.models.find((m) => m.id === config().model) || modelsStore.models[0] const options = modelOptions();
);
if (!isRouter) {
return options.length > 0 ? options[0].model : null;
}
const selectedId = selectedModelId();
if (selectedId) {
const model = options.find((m) => m.id === selectedId);
if (model) return model.model;
}
return null;
});
let hasModelSelected = $derived(!isRouter || !!selectedModelId());
let isSelectedModelInCache = $derived.by(() => {
if (!isRouter) return true;
const currentModelId = selectedModelId();
if (!currentModelId) return false;
return modelOptions().some((option) => option.id === currentModelId);
});
let generateTooltip = $derived.by(() => {
if (!hasModelSelected) {
return 'Please select a model first';
}
if (!isSelectedModelInCache) {
return 'Selected model is not available, please select another';
}
if (inputContent.length == 0) {
return 'Input some text first';
}
return '';
});
let canGenerate = $derived(inputContent.length > 0 && hasModelSelected && isSelectedModelInCache);
let isDisabled = $derived(!canGenerate);
let notebookModel = $state(null);
const { handleModelChange } = useModelChangeValidation({
getRequiredModalities: () => ({ vision: false, audio: false }), // Notebook doesn't require modalities
onSuccess: async (modelName) => {
notebookModel = modelName;
}
});
</script> </script>
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
@ -55,43 +112,55 @@
value={inputContent} value={inputContent}
oninput={handleInput} 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" 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..." placeholder="Enter your text here..."
/> />
</div> </div>
<div class="border-t border-border/40 bg-background p-4 md:px-6 md:py-4"> <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 justify-between gap-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Button {#snippet generateButton(props = {})}
onclick={notebookStore.isGenerating ? handleStop : handleGenerate} <Button
size="sm" disabled={isDisabled}
variant={notebookStore.isGenerating ? 'destructive' : 'default'} onclick={notebookStore.isGenerating ? handleStop : handleGenerate}
class="gap-2" size="sm"
> variant={notebookStore.isGenerating ? 'destructive' : 'default'}
{#if notebookStore.isGenerating} class="gap-2"
<Square class="h-4 w-4 fill-current" /> >
Stop {#if notebookStore.isGenerating}
{:else} <Square class="h-4 w-4 fill-current" />
<Play class="h-4 w-4 fill-current" /> Stop
Generate {:else}
{/if} <Play class="h-4 w-4 fill-current" />
</Button> Generate
{/if}
</Button>
{/snippet}
<Button variant="ghost" size="icon" onclick={() => (modelInfoOpen = true)}> {#if generateTooltip}
<Info class="h-4 w-4" /> <Tooltip.Root>
</Button> <Tooltip.Trigger>
{@render generateButton()}
</Tooltip.Trigger>
<Tooltip.Content>
<p>{generateTooltip}</p>
</Tooltip.Content>
</Tooltip.Root>
{:else}
{@render generateButton()}
{/if}
<ModelsSelector
currentModel={notebookModel}
onModelChange={handleModelChange}
forceForegroundText={true}
useGlobalSelection={true}
disabled={notebookStore.isGenerating}
/>
</div> </div>
<ChatMessageStatistics
predictedTokens={notebookStore.predictedTokens}
predictedMs={notebookStore.predictedMs}
promptTokens={notebookStore.promptTokens}
promptMs={notebookStore.promptMs}
isLive={notebookStore.isGenerating}
/>
</div> </div>
</div> </div>
<DialogChatSettings open={settingsOpen} onOpenChange={(open) => (settingsOpen = open)} /> <DialogChatSettings open={settingsOpen} onOpenChange={(open) => (settingsOpen = open)} />
<DialogModelInformation open={modelInfoOpen} onOpenChange={(open) => (modelInfoOpen = open)} />
</div> </div>

View File

@ -2,70 +2,74 @@ import { ChatService } from '$lib/services/chat';
import { config } from '$lib/stores/settings.svelte'; import { config } from '$lib/stores/settings.svelte';
export class NotebookStore { export class NotebookStore {
content = $state(''); content = $state('');
isGenerating = $state(false); isGenerating = $state(false);
abortController: AbortController | null = null; abortController: AbortController | null = null;
// Statistics // Statistics
promptTokens = $state(0); promptTokens = $state(0);
promptMs = $state(0); promptMs = $state(0);
predictedTokens = $state(0); predictedTokens = $state(0);
predictedMs = $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 // Reset stats
this.promptTokens = 0; this.promptTokens = 0;
this.promptMs = 0; this.promptMs = 0;
this.predictedTokens = 0; this.predictedTokens = 0;
this.predictedMs = 0; this.predictedMs = 0;
try { try {
const currentConfig = config(); const currentConfig = config();
await ChatService.sendCompletion( await ChatService.sendCompletion(
this.content, this.content,
{ {
...currentConfig, ...currentConfig,
model, model: model ?? currentConfig.model,
stream: true, stream: true,
onChunk: (chunk) => { onChunk: (chunk) => {
this.content += chunk; this.content += chunk;
}, },
onTimings: (timings) => { onTimings: (timings) => {
if (timings) { if (timings) {
if (timings.prompt_n) this.promptTokens = timings.prompt_n; if (timings.prompt_n) this.promptTokens = timings.prompt_n;
if (timings.prompt_ms) this.promptMs = timings.prompt_ms; if (timings.prompt_ms) this.promptMs = timings.prompt_ms;
if (timings.predicted_n) this.predictedTokens = timings.predicted_n; if (timings.predicted_n) this.predictedTokens = timings.predicted_n;
if (timings.predicted_ms) this.predictedMs = timings.predicted_ms; if (timings.predicted_ms) this.predictedMs = timings.predicted_ms;
} }
}, },
onComplete: () => { onComplete: () => {
this.isGenerating = false; this.isGenerating = false;
}, },
onError: (error) => { onError: (error) => {
console.error('Notebook generation error:', error); if (error instanceof Error && error.name === 'AbortError') {
this.isGenerating = false; // aborted by user
} } else {
}, console.error('Notebook generation error:', error);
this.abortController.signal }
); this.isGenerating = false;
} catch (error) { }
console.error('Notebook generation failed:', error); },
this.isGenerating = false; this.abortController.signal
} );
} } catch (error) {
console.error('Notebook generation failed:', error);
this.isGenerating = false;
}
}
stop() { stop() {
if (this.abortController) { if (this.abortController) {
this.abortController.abort(); this.abortController.abort();
this.abortController = null; this.abortController = null;
} }
this.isGenerating = false; this.isGenerating = false;
} }
} }
export const notebookStore = new NotebookStore(); export const notebookStore = new NotebookStore();

View File

@ -23,12 +23,12 @@ export interface ApiContextSizeError {
export interface ApiErrorResponse { export interface ApiErrorResponse {
error: error:
| ApiContextSizeError | ApiContextSizeError
| { | {
code: number; code: number;
message: string; message: string;
type?: string; type?: string;
}; };
} }
export interface ApiChatMessageData { export interface ApiChatMessageData {