feat(ui,api): strip board redesign - island, rail by day, context view, server search, vault graph
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { tick } from "svelte";
|
||||
import { tick, untrack } from "svelte";
|
||||
import type { ApiClient } from "$lib/api/client";
|
||||
import type { ContentBlock, HistoryMessage } from "$lib/api/types";
|
||||
import EmptyState from "$lib/components/empty-state.svelte";
|
||||
import ErrorNote from "$lib/components/error-note.svelte";
|
||||
import { Skeleton } from "$lib/components/ui/skeleton";
|
||||
import { Switch } from "$lib/components/ui/switch";
|
||||
import { clip } from "$lib/format";
|
||||
import { cn } from "$lib/utils";
|
||||
import type { ActivityModel } from "./activity.svelte";
|
||||
import { summarizeInput, toolLabel } from "./activity.svelte";
|
||||
import { usePanelHost } from "./host";
|
||||
import Markdown from "./markdown.svelte";
|
||||
import QuestionCard from "./question-card.svelte";
|
||||
import TurnCard from "./turn-card.svelte";
|
||||
@@ -27,6 +26,8 @@
|
||||
} = $props();
|
||||
|
||||
const RESULT_CLIP = 600;
|
||||
const CACHED_MESSAGES = 80;
|
||||
const host = usePanelHost();
|
||||
const NEAR_BOTTOM_PX = 120;
|
||||
const TICK_MS = 1000;
|
||||
|
||||
@@ -53,9 +54,21 @@
|
||||
|
||||
async function load() {
|
||||
failure = null;
|
||||
if (messages === null) {
|
||||
const cached = host.cache?.get<HistoryMessage[]>(
|
||||
`history:${conversationId}`
|
||||
);
|
||||
if (cached) {
|
||||
messages = cached;
|
||||
}
|
||||
}
|
||||
try {
|
||||
({ messages } = await client.history(conversationId));
|
||||
loadedAt = new Date().toISOString();
|
||||
host.cache?.set(
|
||||
`history:${conversationId}`,
|
||||
messages.slice(-CACHED_MESSAGES)
|
||||
);
|
||||
} catch (cause) {
|
||||
failure = cause instanceof Error ? cause.message : String(cause);
|
||||
}
|
||||
@@ -80,7 +93,7 @@
|
||||
$effect(() => {
|
||||
if (refreshKey >= 0) {
|
||||
pinned = true;
|
||||
load();
|
||||
untrack(() => load());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -115,6 +128,28 @@
|
||||
return () => clearInterval(timer);
|
||||
});
|
||||
|
||||
const SYSTEM_HEAD = /^\[[^\]\n]+\]/;
|
||||
let openSystem = $state<Set<number>>(new Set());
|
||||
|
||||
function systemText(message: HistoryMessage): string | null {
|
||||
if (message.role !== "user" || typeof message.content !== "string") {
|
||||
return null;
|
||||
}
|
||||
return SYSTEM_HEAD.test(message.content.trimStart())
|
||||
? message.content
|
||||
: null;
|
||||
}
|
||||
|
||||
function toggleSystem(index: number) {
|
||||
const next = new Set(openSystem);
|
||||
if (next.has(index)) {
|
||||
next.delete(index);
|
||||
} else {
|
||||
next.add(index);
|
||||
}
|
||||
openSystem = next;
|
||||
}
|
||||
|
||||
function blocks(message: HistoryMessage): ContentBlock[] {
|
||||
return typeof message.content === "string"
|
||||
? [{ text: message.content, type: "text" }]
|
||||
@@ -147,20 +182,22 @@
|
||||
bind:this={scroller}
|
||||
>
|
||||
<div class="flex flex-col gap-3" bind:this={body}>
|
||||
<span
|
||||
class="flex items-center gap-2 self-end text-muted-foreground text-xs"
|
||||
<button
|
||||
class="rule-word self-end text-xs"
|
||||
data-active={showResults}
|
||||
onclick={() => {
|
||||
showResults = !showResults;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Switch aria-label="Show tool results" bind:checked={showResults} />
|
||||
show tool results
|
||||
</span>
|
||||
{showResults ? "hide tool results" : "show tool results"}
|
||||
</button>
|
||||
{#if failure}
|
||||
<ErrorNote message={failure} retry={load} />
|
||||
{:else if messages === null}
|
||||
<div class="flex flex-col gap-2">
|
||||
<Skeleton class="h-10 w-2/3" />
|
||||
<Skeleton class="h-16 w-full" />
|
||||
<Skeleton class="h-10 w-1/2" />
|
||||
</div>
|
||||
<p class="py-6 text-center text-muted-foreground text-xs">
|
||||
Loading the thread…
|
||||
</p>
|
||||
{:else if messages.length === 0 && tail.length === 0}
|
||||
<EmptyState
|
||||
hint="Nothing has been said here yet. Write below, or wait for the first inject."
|
||||
@@ -170,7 +207,21 @@
|
||||
{#each messages as message, index (index)}
|
||||
{@const parts = blocks(message)}
|
||||
{@const isResultOnly = parts.every((b) => b.type === "tool_result")}
|
||||
{#if !(isResultOnly && !showResults)}
|
||||
{@const system = systemText(message)}
|
||||
{#if system}
|
||||
<button
|
||||
aria-expanded={openSystem.has(index)}
|
||||
class="self-center max-w-[75ch] rounded-md px-2 py-1 text-left font-mono text-muted-foreground text-xs hover:bg-accent"
|
||||
onclick={() => toggleSystem(index)}
|
||||
type="button"
|
||||
>
|
||||
{#if openSystem.has(index)}
|
||||
<span class="whitespace-pre-wrap break-words">{system}</span>
|
||||
{:else}
|
||||
<span class="truncate">{clip(system.split("\n")[0], 120)}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{:else if !(isResultOnly && !showResults)}
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-col gap-1.5",
|
||||
@@ -181,9 +232,11 @@
|
||||
{#if block.type === "text" && block.text}
|
||||
<Markdown
|
||||
class={cn(
|
||||
"max-w-[75ch] rounded-lg px-3 py-2",
|
||||
message.role === "user" ? "bg-primary/10" : "bg-muted/50"
|
||||
)}
|
||||
"max-w-[75ch]",
|
||||
message.role === "user"
|
||||
? "rounded-2xl rounded-br-md bg-primary/10 px-3.5 py-2"
|
||||
: "px-1 py-1"
|
||||
)}
|
||||
text={block.text}
|
||||
/>
|
||||
{:else if block.type === "tool_use"}
|
||||
|
||||
Reference in New Issue
Block a user