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
/*.code-workspace
/.windsurf/
/.agent/
# emscripten
a.out.*

Binary file not shown.

View File

@ -118,17 +118,6 @@
<ChatSidebarActions {handleMobileSidebarItemClick} bind:isSearchModeActive bind:searchQuery />
</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">
{#if (filteredConversations.length > 0 && isSearchModeActive) || !isSearchModeActive}
<Sidebar.GroupLabel>

View File

@ -1,5 +1,5 @@
<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 { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@ -63,6 +63,18 @@
<KeyboardShortcutInfo keys={['shift', 'cmd', 'o']} />
</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
class="w-full justify-between hover:[&>kbd]:opacity-100"
onclick={() => {

View File

@ -2,19 +2,22 @@
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, Settings, Info } from '@lucide/svelte';
import { Play, Square, Settings } 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';
import { ModelsSelector } from '$lib/components/app';
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 settingsOpen = $state(false);
let modelInfoOpen = $state(false);
let inputContent = $state(content);
let isRouter = $derived(isRouterMode());
// Sync local input with store content
$effect(() => {
inputContent = notebookStore.content;
@ -26,16 +29,70 @@
}
async function handleGenerate() {
await notebookStore.generate();
if (notebookModel == null) {
notebookModel = activeModelId;
}
await notebookStore.generate(notebookModel);
}
function handleStop() {
notebookStore.stop();
}
let currentModel = $derived(
modelsStore.models.find((m) => m.id === config().model) || modelsStore.models[0]
);
let activeModelId = $derived.by(() => {
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>
<div class="flex h-full flex-col">
@ -55,43 +112,55 @@
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..."
placeholder="Enter your text 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>
{#snippet generateButton(props = {})}
<Button
disabled={isDisabled}
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>
{/snippet}
<Button variant="ghost" size="icon" onclick={() => (modelInfoOpen = true)}>
<Info class="h-4 w-4" />
</Button>
{#if generateTooltip}
<Tooltip.Root>
<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>
<ChatMessageStatistics
predictedTokens={notebookStore.predictedTokens}
predictedMs={notebookStore.predictedMs}
promptTokens={notebookStore.promptTokens}
promptMs={notebookStore.promptMs}
isLive={notebookStore.isGenerating}
/>
</div>
</div>
<DialogChatSettings open={settingsOpen} onOpenChange={(open) => (settingsOpen = open)} />
<DialogModelInformation open={modelInfoOpen} onOpenChange={(open) => (modelInfoOpen = open)} />
</div>

View File

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

View File

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