Spaces:
Running
Running
wip dark mode
Browse files- src/lib/components/chat/Assistant.svelte +2 -2
- src/lib/components/chat/Message.svelte +7 -6
- src/lib/components/chat/User.svelte +12 -6
- src/lib/components/chat/markdown/Heading.svelte +4 -4
- src/lib/components/chat/markdown/List.svelte +2 -2
- src/lib/components/chat/markdown/Paragraph.svelte +1 -1
- src/lib/components/flow/actions/PanelRightActions.svelte +12 -3
- src/routes/+layout.svelte +1 -1
- src/routes/+page.svelte +20 -6
- src/routes/api/+server.ts +1 -1
- src/routes/layout.css +4 -0
src/lib/components/chat/Assistant.svelte
CHANGED
|
@@ -15,10 +15,10 @@
|
|
| 15 |
|
| 16 |
let selectedModels = $derived(nodeData.current?.data.selectedModels as ChatModel[] ?? []);
|
| 17 |
let loading = $derived(nodeData.current?.data.loading as boolean ?? false);
|
| 18 |
-
let message = $derived(nodeData.current?.data.content ? { role: 'assistant', content: nodeData.current?.data.content } as ChatMessage : null);
|
| 19 |
</script>
|
| 20 |
|
| 21 |
-
<article class="bg-
|
| 22 |
<div class="nodrag">
|
| 23 |
<header class="flex items-center justify-between mb-3">
|
| 24 |
<div class="flex items-center gap-1 flex-wrap">
|
|
|
|
| 15 |
|
| 16 |
let selectedModels = $derived(nodeData.current?.data.selectedModels as ChatModel[] ?? []);
|
| 17 |
let loading = $derived(nodeData.current?.data.loading as boolean ?? false);
|
| 18 |
+
let message = $derived(nodeData.current?.data.content ? { role: 'assistant', content: nodeData.current?.data.content, timestamp: nodeData.current?.data.timestamp as number ?? 0 } as ChatMessage : null);
|
| 19 |
</script>
|
| 20 |
|
| 21 |
+
<article class="bg-background border border-border shadow-lg/5 p-5 w-[600px] rounded-3xl">
|
| 22 |
<div class="nodrag">
|
| 23 |
<header class="flex items-center justify-between mb-3">
|
| 24 |
<div class="flex items-center gap-1 flex-wrap">
|
src/lib/components/chat/Message.svelte
CHANGED
|
@@ -34,10 +34,11 @@
|
|
| 34 |
</p>
|
| 35 |
{:else}
|
| 36 |
<SvelteMarkdown source={message.content} renderers={renderers as any} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
{/if}
|
| 38 |
-
</main>
|
| 39 |
-
<!-- {#if message.timestamp}
|
| 40 |
-
<p class="text-[10px] text-muted-foreground bg-muted px-2 py-1 rounded-md font-mono">
|
| 41 |
-
{message.timestamp / 1000}s
|
| 42 |
-
</p>
|
| 43 |
-
{/if} -->
|
|
|
|
| 34 |
</p>
|
| 35 |
{:else}
|
| 36 |
<SvelteMarkdown source={message.content} renderers={renderers as any} />
|
| 37 |
+
{#if message.timestamp}
|
| 38 |
+
<p class="text-xs text-muted-foreground/70">
|
| 39 |
+
- Generated in
|
| 40 |
+
{message.timestamp / 1000}s
|
| 41 |
+
</p>
|
| 42 |
+
{/if}
|
| 43 |
{/if}
|
| 44 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/components/chat/User.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { Send, X } from '@lucide/svelte';
|
| 3 |
import { Handle, useEdges, useNodes, useNodesData, Position, type NodeProps, type Edge , type Node, useSvelteFlow} from '@xyflow/svelte';
|
|
|
|
| 4 |
|
| 5 |
import type { ChatModel, ChatMessage } from '$lib/helpers/types';
|
| 6 |
import { Button } from '$lib/components/ui/button';
|
|
@@ -25,19 +26,22 @@
|
|
| 25 |
function addModel(model: ChatModel) {
|
| 26 |
if (!selectedModels.some((m) => m.id === model.id)) {
|
| 27 |
selectedModels = [...selectedModels, model];
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
}
|
| 30 |
function removeModel(model: ChatModel) {
|
| 31 |
selectedModels = selectedModels.filter((m) => m.id !== model.id);
|
| 32 |
}
|
| 33 |
|
| 34 |
-
function handleTriggerAction() {
|
| 35 |
const newNodes: Node[] = [];
|
| 36 |
const newEdges: Edge[] = [];
|
| 37 |
messages = [...messages, { role: 'user', content: prompt }];
|
| 38 |
updateNodeData(id, { messages }, { replace: true });
|
| 39 |
|
| 40 |
-
|
| 41 |
const newNodeId = `assistant-${crypto.randomUUID()}`;
|
| 42 |
const newNode: Node = {
|
| 43 |
id: newNodeId,
|
|
@@ -73,11 +77,11 @@
|
|
| 73 |
}
|
| 74 |
|
| 75 |
async function handleTriggerAiCall(newNodes: Node[]) {
|
| 76 |
-
const start = Date.now();
|
| 77 |
newNodes.forEach(async (node) => {
|
| 78 |
const model = (node?.data?.selectedModels as ChatModel[])?.length > 0 ? (node?.data?.selectedModels as ChatModel[])[0] : null;
|
| 79 |
if (!model) return;
|
| 80 |
try {
|
|
|
|
| 81 |
const response = await fetch('/api', {
|
| 82 |
method: 'POST',
|
| 83 |
body: JSON.stringify({
|
|
@@ -117,6 +121,8 @@
|
|
| 117 |
}
|
| 118 |
updateNodes((currentNodes) => [...currentNodes, newNode]);
|
| 119 |
updateEdges((currentEdges) => [...currentEdges, newEdge]);
|
|
|
|
|
|
|
| 120 |
break;
|
| 121 |
}
|
| 122 |
|
|
@@ -134,7 +140,7 @@
|
|
| 134 |
let lastMessage = $derived(messages?.length > 0 && messages[messages.length - 1].role === 'user' ? messages[messages.length - 1] : null);
|
| 135 |
</script>
|
| 136 |
|
| 137 |
-
<article class="bg-
|
| 138 |
<div class="nodrag">
|
| 139 |
<header class="flex items-center justify-between mb-3">
|
| 140 |
<div class="flex items-center gap-1 flex-wrap">
|
|
@@ -149,7 +155,7 @@
|
|
| 149 |
{/if}
|
| 150 |
</Button>
|
| 151 |
{/each}
|
| 152 |
-
{#if selectedModels.length < 3 && !loading
|
| 153 |
<ComboBoxModels
|
| 154 |
onSelect={addModel}
|
| 155 |
excludeIds={selectedModels.map((m) => m.id)}
|
|
@@ -178,7 +184,7 @@
|
|
| 178 |
}
|
| 179 |
}}
|
| 180 |
></textarea>
|
| 181 |
-
<Button variant=
|
| 182 |
{#if loading}
|
| 183 |
<Spinner className="size-5"/>
|
| 184 |
{:else}
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { Send, X } from '@lucide/svelte';
|
| 3 |
import { Handle, useEdges, useNodes, useNodesData, Position, type NodeProps, type Edge , type Node, useSvelteFlow} from '@xyflow/svelte';
|
| 4 |
+
import { mode } from 'mode-watcher';
|
| 5 |
|
| 6 |
import type { ChatModel, ChatMessage } from '$lib/helpers/types';
|
| 7 |
import { Button } from '$lib/components/ui/button';
|
|
|
|
| 26 |
function addModel(model: ChatModel) {
|
| 27 |
if (!selectedModels.some((m) => m.id === model.id)) {
|
| 28 |
selectedModels = [...selectedModels, model];
|
| 29 |
+
if (lastMessage) {
|
| 30 |
+
handleTriggerAction([model]);
|
| 31 |
+
}
|
| 32 |
}
|
| 33 |
}
|
| 34 |
function removeModel(model: ChatModel) {
|
| 35 |
selectedModels = selectedModels.filter((m) => m.id !== model.id);
|
| 36 |
}
|
| 37 |
|
| 38 |
+
function handleTriggerAction(models: ChatModel[] = selectedModels) {
|
| 39 |
const newNodes: Node[] = [];
|
| 40 |
const newEdges: Edge[] = [];
|
| 41 |
messages = [...messages, { role: 'user', content: prompt }];
|
| 42 |
updateNodeData(id, { messages }, { replace: true });
|
| 43 |
|
| 44 |
+
models.forEach((m) => {
|
| 45 |
const newNodeId = `assistant-${crypto.randomUUID()}`;
|
| 46 |
const newNode: Node = {
|
| 47 |
id: newNodeId,
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
async function handleTriggerAiCall(newNodes: Node[]) {
|
|
|
|
| 80 |
newNodes.forEach(async (node) => {
|
| 81 |
const model = (node?.data?.selectedModels as ChatModel[])?.length > 0 ? (node?.data?.selectedModels as ChatModel[])[0] : null;
|
| 82 |
if (!model) return;
|
| 83 |
try {
|
| 84 |
+
const start = Date.now();
|
| 85 |
const response = await fetch('/api', {
|
| 86 |
method: 'POST',
|
| 87 |
body: JSON.stringify({
|
|
|
|
| 121 |
}
|
| 122 |
updateNodes((currentNodes) => [...currentNodes, newNode]);
|
| 123 |
updateEdges((currentEdges) => [...currentEdges, newEdge]);
|
| 124 |
+
const end = Date.now();
|
| 125 |
+
updateNodeData(node.id, { ...node.data, content, timestamp: end - start, loading: false }, { replace: true });
|
| 126 |
break;
|
| 127 |
}
|
| 128 |
|
|
|
|
| 140 |
let lastMessage = $derived(messages?.length > 0 && messages[messages.length - 1].role === 'user' ? messages[messages.length - 1] : null);
|
| 141 |
</script>
|
| 142 |
|
| 143 |
+
<article class="bg-background border border-border shadow-lg/5 p-5 w-[600px] rounded-3xl">
|
| 144 |
<div class="nodrag">
|
| 145 |
<header class="flex items-center justify-between mb-3">
|
| 146 |
<div class="flex items-center gap-1 flex-wrap">
|
|
|
|
| 155 |
{/if}
|
| 156 |
</Button>
|
| 157 |
{/each}
|
| 158 |
+
{#if selectedModels.length < 3 && !loading}
|
| 159 |
<ComboBoxModels
|
| 160 |
onSelect={addModel}
|
| 161 |
excludeIds={selectedModels.map((m) => m.id)}
|
|
|
|
| 184 |
}
|
| 185 |
}}
|
| 186 |
></textarea>
|
| 187 |
+
<Button variant={mode.current === 'dark' ? 'default' : 'outline'} size="icon-sm" class="" disabled={!selectedModels.length || !prompt || loading} onclick={() => handleTriggerAction()}>
|
| 188 |
{#if loading}
|
| 189 |
<Spinner className="size-5"/>
|
| 190 |
{:else}
|
src/lib/components/chat/markdown/Heading.svelte
CHANGED
|
@@ -4,11 +4,11 @@
|
|
| 4 |
</script>
|
| 5 |
|
| 6 |
{#if depth === 1}
|
| 7 |
-
<h1 class="text-
|
| 8 |
{:else if depth === 2}
|
| 9 |
-
<h2 class="text-
|
| 10 |
{:else if depth === 3}
|
| 11 |
-
<h3 class="text-
|
| 12 |
{:else}
|
| 13 |
-
<h4 class="text-
|
| 14 |
{/if}
|
|
|
|
| 4 |
</script>
|
| 5 |
|
| 6 |
{#if depth === 1}
|
| 7 |
+
<h1 class="text-2xl font-semibold text-foreground mt-4 mb-2 first:mt-0">{@render children?.()}</h1>
|
| 8 |
{:else if depth === 2}
|
| 9 |
+
<h2 class="text-xl font-semibold text-foreground mt-3.5 mb-2 first:mt-0">{@render children?.()}</h2>
|
| 10 |
{:else if depth === 3}
|
| 11 |
+
<h3 class="text-lg font-semibold text-foreground mt-3 mb-1.5 first:mt-0">{@render children?.()}</h3>
|
| 12 |
{:else}
|
| 13 |
+
<h4 class="text-base font-semibold text-foreground mt-2.5 mb-1 first:mt-0">{@render children?.()}</h4>
|
| 14 |
{/if}
|
src/lib/components/chat/markdown/List.svelte
CHANGED
|
@@ -4,11 +4,11 @@
|
|
| 4 |
</script>
|
| 5 |
|
| 6 |
{#if ordered}
|
| 7 |
-
<ol class="list-decimal list-outside pl-5 my-2 space-y-1 text-
|
| 8 |
{@render children?.()}
|
| 9 |
</ol>
|
| 10 |
{:else}
|
| 11 |
-
<ul class="list-disc list-outside pl-5 my-2 space-y-1 text-
|
| 12 |
{@render children?.()}
|
| 13 |
</ul>
|
| 14 |
{/if}
|
|
|
|
| 4 |
</script>
|
| 5 |
|
| 6 |
{#if ordered}
|
| 7 |
+
<ol class="list-decimal list-outside pl-5 my-2 space-y-1 text-base text-foreground/90 marker:text-muted-foreground" {start}>
|
| 8 |
{@render children?.()}
|
| 9 |
</ol>
|
| 10 |
{:else}
|
| 11 |
+
<ul class="list-disc list-outside pl-5 my-2 space-y-1 text-base text-foreground/90 marker:text-muted-foreground">
|
| 12 |
{@render children?.()}
|
| 13 |
</ul>
|
| 14 |
{/if}
|
src/lib/components/chat/markdown/Paragraph.svelte
CHANGED
|
@@ -3,4 +3,4 @@
|
|
| 3 |
let { children }: { children?: Snippet } = $props();
|
| 4 |
</script>
|
| 5 |
|
| 6 |
-
<p class="text-
|
|
|
|
| 3 |
let { children }: { children?: Snippet } = $props();
|
| 4 |
</script>
|
| 5 |
|
| 6 |
+
<p class="text-base leading-relaxed text-foreground/90 mb-3 last:mb-0">{@render children?.()}</p>
|
src/lib/components/flow/actions/PanelRightActions.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import {
|
| 3 |
import { Panel } from '@xyflow/svelte';
|
|
|
|
| 4 |
|
| 5 |
import { Button } from '$lib/components/ui/button';
|
| 6 |
|
|
@@ -10,13 +11,21 @@
|
|
| 10 |
function handleReset() {
|
| 11 |
const ok = confirm('Are you sure you want to reset the flow?');
|
| 12 |
if (ok) {
|
| 13 |
-
|
| 14 |
}
|
| 15 |
}
|
| 16 |
</script>
|
| 17 |
|
| 18 |
<Panel position="top-right" class="p-3 flex items-center justify-end gap-2">
|
| 19 |
<Button variant="outline" size="icon" class="" onclick={handleReset}>
|
| 20 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
</Button>
|
| 22 |
</Panel>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { Cog, Contrast, RefreshCcw } from '@lucide/svelte';
|
| 3 |
import { Panel } from '@xyflow/svelte';
|
| 4 |
+
import { toggleMode } from 'mode-watcher';
|
| 5 |
|
| 6 |
import { Button } from '$lib/components/ui/button';
|
| 7 |
|
|
|
|
| 11 |
function handleReset() {
|
| 12 |
const ok = confirm('Are you sure you want to reset the flow?');
|
| 13 |
if (ok) {
|
| 14 |
+
location.reload();
|
| 15 |
}
|
| 16 |
}
|
| 17 |
</script>
|
| 18 |
|
| 19 |
<Panel position="top-right" class="p-3 flex items-center justify-end gap-2">
|
| 20 |
<Button variant="outline" size="icon" class="" onclick={handleReset}>
|
| 21 |
+
<RefreshCcw />
|
| 22 |
+
</Button>
|
| 23 |
+
<Button variant="outline" size="icon" class="" onclick={() => toggleMode()}>
|
| 24 |
+
<Contrast />
|
| 25 |
+
</Button>
|
| 26 |
+
<Button variant="outline" size="icon" class="" onclick={() => {
|
| 27 |
+
alert("Coming soon...")
|
| 28 |
+
}}>
|
| 29 |
+
<Cog />
|
| 30 |
</Button>
|
| 31 |
</Panel>
|
src/routes/+layout.svelte
CHANGED
|
@@ -34,7 +34,7 @@
|
|
| 34 |
{#if modelsState.loading}
|
| 35 |
<MainLoading />
|
| 36 |
{:else}
|
| 37 |
-
|
| 38 |
{/if}
|
| 39 |
</div>
|
| 40 |
</svelte:boundary>
|
|
|
|
| 34 |
{#if modelsState.loading}
|
| 35 |
<MainLoading />
|
| 36 |
{:else}
|
| 37 |
+
{@render children?.()}
|
| 38 |
{/if}
|
| 39 |
</div>
|
| 40 |
</svelte:boundary>
|
src/routes/+page.svelte
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import { SvelteFlow, Background, MiniMap, Panel, BackgroundVariant, type Node, type Edge } from '@xyflow/svelte';
|
| 3 |
import '@xyflow/svelte/dist/style.css';
|
| 4 |
-
|
|
|
|
| 5 |
import { modelsState } from '$lib/state/models.svelte';
|
| 6 |
import User from '$lib/components/chat/User.svelte';
|
| 7 |
import Assistant from '$lib/components/chat/Assistant.svelte';
|
|
@@ -39,6 +40,7 @@
|
|
| 39 |
minZoom={0.8}
|
| 40 |
maxZoom={1.5}
|
| 41 |
fitView
|
|
|
|
| 42 |
proOptions={{ hideAttribution: true }}
|
| 43 |
fitViewOptions={{
|
| 44 |
maxZoom: 1,
|
|
@@ -46,12 +48,24 @@
|
|
| 46 |
interpolate: "smooth",
|
| 47 |
duration: 500,
|
| 48 |
}}
|
| 49 |
-
defaultEdgeOptions={{ type:
|
| 50 |
-
class="bg-
|
|
|
|
| 51 |
>
|
| 52 |
<FitViewOnResize initialNodes={initialNodes} />
|
| 53 |
-
<MiniMap
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
<PanelRightActions
|
| 56 |
onReset={() => {
|
| 57 |
nodes = getInitialNodes();
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { SvelteFlow, Background, MiniMap, Panel, BackgroundVariant, type Node, type Edge, type ColorMode } from '@xyflow/svelte';
|
| 3 |
import '@xyflow/svelte/dist/style.css';
|
| 4 |
+
import { theme, mode } from 'mode-watcher';
|
| 5 |
+
|
| 6 |
import { modelsState } from '$lib/state/models.svelte';
|
| 7 |
import User from '$lib/components/chat/User.svelte';
|
| 8 |
import Assistant from '$lib/components/chat/Assistant.svelte';
|
|
|
|
| 40 |
minZoom={0.8}
|
| 41 |
maxZoom={1.5}
|
| 42 |
fitView
|
| 43 |
+
colorMode={mode.current}
|
| 44 |
proOptions={{ hideAttribution: true }}
|
| 45 |
fitViewOptions={{
|
| 46 |
maxZoom: 1,
|
|
|
|
| 48 |
interpolate: "smooth",
|
| 49 |
duration: 500,
|
| 50 |
}}
|
| 51 |
+
defaultEdgeOptions={{ type: "bezier" }}
|
| 52 |
+
class="bg-background!"
|
| 53 |
+
|
| 54 |
>
|
| 55 |
<FitViewOnResize initialNodes={initialNodes} />
|
| 56 |
+
<MiniMap
|
| 57 |
+
nodeColor={mode.current === 'dark' ? 'rgba(255, 255, 255, 0.07)' : 'rgba(0, 0, 0, 0.07)'}
|
| 58 |
+
maskColor={mode.current === 'dark' ? 'rgba(255, 255, 255, 0.07)' : 'rgba(0, 0, 0, 0.05)'}
|
| 59 |
+
class="rounded-xl! dark:bg-gray-950! overflow-hidden!"
|
| 60 |
+
/>
|
| 61 |
+
<Background
|
| 62 |
+
variant={BackgroundVariant.Lines}
|
| 63 |
+
gap={30}
|
| 64 |
+
patternColor={
|
| 65 |
+
mode.current === 'dark' ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)'
|
| 66 |
+
}
|
| 67 |
+
class="bg-background!"
|
| 68 |
+
/>
|
| 69 |
<PanelRightActions
|
| 70 |
onReset={() => {
|
| 71 |
nodes = getInitialNodes();
|
src/routes/api/+server.ts
CHANGED
|
@@ -15,7 +15,7 @@ export async function POST({ request }: RequestEvent) {
|
|
| 15 |
const stream = client.chatCompletionStream({
|
| 16 |
model: model,
|
| 17 |
messages: [
|
| 18 |
-
{ role: 'system', content: 'You are a helpful assistant. You are very helpful and friendly. Use markdown to format your responses.' },
|
| 19 |
...(messages ?? []),
|
| 20 |
{ role: 'user', content: prompt }
|
| 21 |
],
|
|
|
|
| 15 |
const stream = client.chatCompletionStream({
|
| 16 |
model: model,
|
| 17 |
messages: [
|
| 18 |
+
{ role: 'system', content: 'You are a helpful assistant. You are very helpful and friendly. Use markdown to format your responses, but don\'t include array start and end markers.' },
|
| 19 |
...(messages ?? []),
|
| 20 |
{ role: 'user', content: prompt }
|
| 21 |
],
|
src/routes/layout.css
CHANGED
|
@@ -119,3 +119,7 @@
|
|
| 119 |
@apply bg-background text-foreground;
|
| 120 |
}
|
| 121 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
@apply bg-background text-foreground;
|
| 120 |
}
|
| 121 |
}
|
| 122 |
+
|
| 123 |
+
.dark .svelte-flow__edge-path {
|
| 124 |
+
@apply stroke-gray-800;
|
| 125 |
+
}
|