enzostvs HF Staff commited on
Commit
f7c8859
·
1 Parent(s): 72b54f8

responsive

Browse files
src/lib/components/chat/User.svelte CHANGED
@@ -19,6 +19,9 @@
19
  import Spinner from '$lib/components/loading/Spinner.svelte';
20
  import Message from './Message.svelte';
21
  import SettingsModel from '$lib/components/model/SettingsModel.svelte';
 
 
 
22
 
23
  let { id }: NodeProps = $props();
24
 
@@ -31,18 +34,23 @@
31
  let selectedModels = $derived<ChatModel[]>(
32
  (nodeData.current?.data.selectedModels as ChatModel[]) ?? []
33
  );
 
34
  let prompt = $state.raw<string>((nodeData.current?.data.prompt as string) ?? '');
35
  let loading = $state.raw<boolean>(false);
36
  let messages = $state.raw<ChatMessage[]>(
37
  (nodeData.current?.data?.messages as ChatMessage[]) ?? []
38
  );
 
 
 
 
39
 
40
  function addModel(model: ChatModel) {
41
  if (!selectedModels.some((m) => m.id === model.id)) {
42
  updateNodeData(
43
  id,
44
  {
45
- ...nodeData.current,
46
  selectedModels: [...selectedModels, model]
47
  },
48
  { replace: true }
@@ -56,7 +64,7 @@
56
  updateNodeData(
57
  id,
58
  {
59
- ...nodeData.current,
60
  selectedModels: selectedModels.filter((m) => m.id !== model.id)
61
  },
62
  { replace: true }
@@ -95,12 +103,6 @@
95
  });
96
  updateNodes((currentNodes) => [...currentNodes, ...newNodes]);
97
  updateEdges((currentEdges) => [...currentEdges, ...newEdges]);
98
- fitView({
99
- maxZoom: 1,
100
- minZoom: 1,
101
- interpolate: 'smooth',
102
- duration: 500
103
- });
104
  handleTriggerAiCall(newNodes);
105
  }
106
 
@@ -114,8 +116,8 @@
114
  method: 'POST',
115
  body: JSON.stringify({
116
  model: model.id,
117
- prompt,
118
  provider: model.provider,
 
119
  options: {
120
  temperature: model.temperature,
121
  max_tokens: model.max_tokens,
@@ -182,7 +184,9 @@
182
  );
183
  </script>
184
 
185
- <article class="w-[600px] rounded-3xl border border-border bg-background p-5 shadow-lg/5">
 
 
186
  <div class="nodrag cursor-auto">
187
  <header class="mb-3 flex items-center justify-between">
188
  <div class="flex flex-wrap items-center gap-1">
@@ -221,7 +225,7 @@
221
  </Button>
222
  </SettingsModel>
223
  {/each}
224
- {#if selectedModels.length < 3 && !loading}
225
  <ComboBoxModels onSelect={addModel} excludeIds={selectedModels.map((m) => m.id)} />
226
  {/if}
227
  </div>
@@ -247,19 +251,41 @@
247
  }
248
  }}
249
  ></textarea>
250
- <Button
251
- variant={mode.current === 'dark' ? 'default' : 'outline'}
252
- size="icon-sm"
253
- class=""
254
- disabled={!selectedModels.length || !prompt || loading}
255
- onclick={() => handleTriggerAction()}
256
- >
257
- {#if loading}
258
- <Spinner className="size-5" />
 
 
 
 
 
 
 
 
 
259
  {:else}
260
- <Send />
261
  {/if}
262
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  </footer>
264
  {/if}
265
  </div>
 
19
  import Spinner from '$lib/components/loading/Spinner.svelte';
20
  import Message from './Message.svelte';
21
  import SettingsModel from '$lib/components/model/SettingsModel.svelte';
22
+ import { MAX_MODELS_PER_NODE, MAX_SUGGESTIONS } from '$lib';
23
+ import { SUGGESTIONS_PROMPT } from '$lib/consts';
24
+ import { breakpointsState } from '$lib/state/breakpoints.svelte';
25
 
26
  let { id }: NodeProps = $props();
27
 
 
34
  let selectedModels = $derived<ChatModel[]>(
35
  (nodeData.current?.data.selectedModels as ChatModel[]) ?? []
36
  );
37
+ let isFirstNode = $derived((nodeData.current?.data.isFirstNode as boolean) ?? false);
38
  let prompt = $state.raw<string>((nodeData.current?.data.prompt as string) ?? '');
39
  let loading = $state.raw<boolean>(false);
40
  let messages = $state.raw<ChatMessage[]>(
41
  (nodeData.current?.data?.messages as ChatMessage[]) ?? []
42
  );
43
+ let randomSuggestions = SUGGESTIONS_PROMPT.sort(() => Math.random() - 0.5).slice(
44
+ 0,
45
+ MAX_SUGGESTIONS
46
+ );
47
 
48
  function addModel(model: ChatModel) {
49
  if (!selectedModels.some((m) => m.id === model.id)) {
50
  updateNodeData(
51
  id,
52
  {
53
+ ...nodeData.current?.data,
54
  selectedModels: [...selectedModels, model]
55
  },
56
  { replace: true }
 
64
  updateNodeData(
65
  id,
66
  {
67
+ ...nodeData.current?.data,
68
  selectedModels: selectedModels.filter((m) => m.id !== model.id)
69
  },
70
  { replace: true }
 
103
  });
104
  updateNodes((currentNodes) => [...currentNodes, ...newNodes]);
105
  updateEdges((currentEdges) => [...currentEdges, ...newEdges]);
 
 
 
 
 
 
106
  handleTriggerAiCall(newNodes);
107
  }
108
 
 
116
  method: 'POST',
117
  body: JSON.stringify({
118
  model: model.id,
 
119
  provider: model.provider,
120
+ messages,
121
  options: {
122
  temperature: model.temperature,
123
  max_tokens: model.max_tokens,
 
184
  );
185
  </script>
186
 
187
+ <article
188
+ class="w-full min-w-sm rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]"
189
+ >
190
  <div class="nodrag cursor-auto">
191
  <header class="mb-3 flex items-center justify-between">
192
  <div class="flex flex-wrap items-center gap-1">
 
225
  </Button>
226
  </SettingsModel>
227
  {/each}
228
+ {#if selectedModels.length < MAX_MODELS_PER_NODE && !loading}
229
  <ComboBoxModels onSelect={addModel} excludeIds={selectedModels.map((m) => m.id)} />
230
  {/if}
231
  </div>
 
251
  }
252
  }}
253
  ></textarea>
254
+ <div class="flex w-full items-center justify-between gap-1">
255
+ {#if isFirstNode && !loading && !lastMessage}
256
+ <div class="items flex w-full gap-1">
257
+ {#each randomSuggestions as suggestion}
258
+ <Button
259
+ variant="outline"
260
+ size="2xs"
261
+ class="rounded-full! shadow-none!"
262
+ disabled={!selectedModels.length}
263
+ onclick={() => {
264
+ prompt = suggestion;
265
+ handleTriggerAction();
266
+ }}
267
+ >
268
+ {suggestion}
269
+ </Button>
270
+ {/each}
271
+ </div>
272
  {:else}
273
+ <div></div>
274
  {/if}
275
+ <Button
276
+ variant={mode.current === 'dark' ? 'default' : 'outline'}
277
+ size="icon-sm"
278
+ class=""
279
+ disabled={!selectedModels.length || !prompt || loading}
280
+ onclick={() => handleTriggerAction()}
281
+ >
282
+ {#if loading}
283
+ <Spinner className="size-5" />
284
+ {:else}
285
+ <Send />
286
+ {/if}
287
+ </Button>
288
+ </div>
289
  </footer>
290
  {/if}
291
  </div>
src/lib/components/flow/FitViewOnResize.svelte CHANGED
@@ -3,14 +3,15 @@
3
  import type { Node, Edge } from '@xyflow/svelte';
4
  import { useNodes, useEdges } from '@xyflow/svelte';
5
  import { useSvelteFlow } from '@xyflow/svelte';
 
6
 
7
  let { initialNodes }: { initialNodes: Node[] } = $props();
8
 
9
  // Fallback dimensions (used before nodes are measured by xyflow)
10
- const DEFAULT_WIDTH = 600;
11
  const DEFAULT_HEIGHT = 200;
12
- const H_SPACING = 40;
13
- const V_SPACING = 40;
14
 
15
  const { fitView } = useSvelteFlow();
16
  const nodesStore = useNodes();
@@ -68,7 +69,7 @@
68
  function handleWindowResize() {
69
  fitView({
70
  maxZoom: 1,
71
- minZoom: 0.8,
72
  interpolate: 'smooth',
73
  duration: 500
74
  });
@@ -198,7 +199,7 @@
198
 
199
  fitView({
200
  maxZoom: 1,
201
- minZoom: 0.8,
202
  interpolate: 'smooth',
203
  duration: 250
204
  });
 
3
  import type { Node, Edge } from '@xyflow/svelte';
4
  import { useNodes, useEdges } from '@xyflow/svelte';
5
  import { useSvelteFlow } from '@xyflow/svelte';
6
+ import { breakpointsState } from '$lib/state/breakpoints.svelte';
7
 
8
  let { initialNodes }: { initialNodes: Node[] } = $props();
9
 
10
  // Fallback dimensions (used before nodes are measured by xyflow)
11
+ const DEFAULT_WIDTH = breakpointsState.isMobile ? 300 : 600;
12
  const DEFAULT_HEIGHT = 200;
13
+ const H_SPACING = 60;
14
+ const V_SPACING = 60;
15
 
16
  const { fitView } = useSvelteFlow();
17
  const nodesStore = useNodes();
 
69
  function handleWindowResize() {
70
  fitView({
71
  maxZoom: 1,
72
+ minZoom: breakpointsState.isMobile ? 1 : 0.8,
73
  interpolate: 'smooth',
74
  duration: 500
75
  });
 
199
 
200
  fitView({
201
  maxZoom: 1,
202
+ minZoom: breakpointsState.isMobile ? 1 : 0.8,
203
  interpolate: 'smooth',
204
  duration: 250
205
  });
src/lib/components/flow/actions/PanelRightActions.svelte CHANGED
@@ -7,7 +7,10 @@
7
  import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
8
  import { tokenModalState } from '$lib/state/token-modal.svelte';
9
 
 
 
10
  function handleReset() {
 
11
  const ok = confirm('Are you sure you want to reset the flow?');
12
  if (ok) {
13
  location.reload();
@@ -15,9 +18,10 @@
15
  }
16
  </script>
17
 
18
- <Panel position="top-right" class="flex items-center justify-end gap-2 p-3">
19
- <Button variant="outline" size="icon" class="" onclick={handleReset}>
20
  <RefreshCcw />
 
21
  </Button>
22
  <DropdownMenu.Root>
23
  <DropdownMenu.Trigger>
@@ -29,10 +33,10 @@
29
  </DropdownMenu.Trigger>
30
  <DropdownMenu.Content class="w-56" align="start">
31
  <DropdownMenu.Group>
32
- <DropdownMenu.Item onclick={() => (tokenModalState.open = true)}>
33
- Token Management
34
- <DropdownMenu.Shortcut>⇧⌘T</DropdownMenu.Shortcut>
35
- </DropdownMenu.Item>
36
  </DropdownMenu.Group>
37
  <DropdownMenu.Separator />
38
  <DropdownMenu.Group>
@@ -42,11 +46,11 @@
42
  Appearance
43
  </DropdownMenu.SubTrigger>
44
  <DropdownMenu.SubContent>
45
- <DropdownMenu.Item onclick={() => setMode('light')}><Sun />Light</DropdownMenu.Item>
46
- <DropdownMenu.Item onclick={() => setMode('dark')}><Moon />Dark</DropdownMenu.Item>
47
- <DropdownMenu.Item onclick={() => setMode('system')}
48
- ><Monitor />System</DropdownMenu.Item
49
- >
50
  </DropdownMenu.SubContent>
51
  </DropdownMenu.Sub>
52
  </DropdownMenu.Group>
 
7
  import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
8
  import { tokenModalState } from '$lib/state/token-modal.svelte';
9
 
10
+ let { canReset }: { canReset: boolean } = $props();
11
+
12
  function handleReset() {
13
+ if (!canReset) return;
14
  const ok = confirm('Are you sure you want to reset the flow?');
15
  if (ok) {
16
  location.reload();
 
18
  }
19
  </script>
20
 
21
+ <Panel position="top-right" class="flex items-center justify-end gap-2 p-1 lg:p-3">
22
+ <Button variant="outline" class="" onclick={handleReset}>
23
  <RefreshCcw />
24
+ Start new chat
25
  </Button>
26
  <DropdownMenu.Root>
27
  <DropdownMenu.Trigger>
 
33
  </DropdownMenu.Trigger>
34
  <DropdownMenu.Content class="w-56" align="start">
35
  <DropdownMenu.Group>
36
+ <DropdownMenu.Item onclick={() => (tokenModalState.open = true)}>
37
+ Token Management
38
+ <DropdownMenu.Shortcut>⇧⌘T</DropdownMenu.Shortcut>
39
+ </DropdownMenu.Item>
40
  </DropdownMenu.Group>
41
  <DropdownMenu.Separator />
42
  <DropdownMenu.Group>
 
46
  Appearance
47
  </DropdownMenu.SubTrigger>
48
  <DropdownMenu.SubContent>
49
+ <DropdownMenu.Item onclick={() => setMode('light')}><Sun />Light</DropdownMenu.Item>
50
+ <DropdownMenu.Item onclick={() => setMode('dark')}><Moon />Dark</DropdownMenu.Item>
51
+ <DropdownMenu.Item onclick={() => setMode('system')}
52
+ ><Monitor />System</DropdownMenu.Item
53
+ >
54
  </DropdownMenu.SubContent>
55
  </DropdownMenu.Sub>
56
  </DropdownMenu.Group>
src/lib/components/model/ComboBoxModels.svelte CHANGED
@@ -5,6 +5,7 @@
5
  import { Button } from '$lib/components/ui/button/';
6
  import { modelsState } from '$lib/state/models.svelte';
7
  import type { ChatModel } from '$lib/helpers/types';
 
8
 
9
  interface Props {
10
  onSelect?: (model: ChatModel) => void;
@@ -23,8 +24,6 @@
23
  })
24
  );
25
 
26
- const MAX_TRENDING_MODELS = 6;
27
-
28
  let trendingModels = $derived(filteredModels.slice(0, MAX_TRENDING_MODELS));
29
  let otherModels = $derived(filteredModels.slice(MAX_TRENDING_MODELS));
30
  </script>
 
5
  import { Button } from '$lib/components/ui/button/';
6
  import { modelsState } from '$lib/state/models.svelte';
7
  import type { ChatModel } from '$lib/helpers/types';
8
+ import { MAX_TRENDING_MODELS } from '$lib';
9
 
10
  interface Props {
11
  onSelect?: (model: ChatModel) => void;
 
24
  })
25
  );
26
 
 
 
27
  let trendingModels = $derived(filteredModels.slice(0, MAX_TRENDING_MODELS));
28
  let otherModels = $derived(filteredModels.slice(MAX_TRENDING_MODELS));
29
  </script>
src/lib/consts.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const SUGGESTIONS_PROMPT = [
2
+ 'Roast my code skills',
3
+ 'Why do LLMs hallucinate?',
4
+ 'Explain AI like I\'m 5',
5
+ 'Is a hotdog a sandwich?',
6
+ 'Pineapple on pizza: yes or no?',
7
+ 'Write me a bad poem',
8
+ 'Why is CSS so painful?',
9
+ 'Tell me a dad joke',
10
+ 'Best excuse to skip work?',
11
+ 'Are robots coming for us?',
12
+ 'Why do cats judge us?',
13
+ 'Convince me earth is flat',
14
+ 'Invent a new pasta shape',
15
+ 'What would dogs google?',
16
+ 'Why is Monday the worst?',
17
+ 'Rate my mass: 70kg',
18
+ 'Pick my lunch for me',
19
+ 'Write a haiku about bugs',
20
+ 'Am I in a simulation?',
21
+ 'Explain blockchain to my grandma',
22
+ 'Why do devs love coffee?',
23
+ 'Sell me this pen',
24
+ 'Make up a conspiracy theory',
25
+ 'Worst date idea ever?',
26
+ 'Name a fake startup',
27
+ 'Describe WiFi to a pirate',
28
+ 'Why do we yawn?',
29
+ 'Invent a useless invention',
30
+ 'Teach me a fun fact',
31
+ 'What came first: egg or chicken?'
32
+ ];
src/lib/helpers/nodes-and-edges.ts DELETED
File without changes
src/lib/helpers/utils.ts DELETED
@@ -1,6 +0,0 @@
1
- import { type ClassValue, clsx } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
 
 
 
 
 
 
 
src/lib/index.ts CHANGED
@@ -1 +1,4 @@
1
- // place files you want to import through the `$lib` alias in this folder.
 
 
 
 
1
+ export const MAX_MODELS_PER_NODE = 5;
2
+ export const MAX_TRENDING_MODELS = 6;
3
+ export const MAX_SUGGESTIONS = 2;
4
+ export const MAX_DEFAULT_MODELS = 1;
src/lib/state/breakpoints.svelte.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export const breakpointsState = $state({
2
+ isMobile: false
3
+ });
src/routes/+error.svelte ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ </script>
4
+
5
+ <h1>{page.status} {page.error.message}</h1>
6
+ <span style="font-size: 10em"> 🙅 </span>
src/routes/+layout.svelte CHANGED
@@ -7,6 +7,7 @@
7
  import { fetchModels, modelsState } from '$lib/state/models.svelte';
8
  import MainLoading from '$lib/components/loading/MainLoading.svelte';
9
  import TokenManagementModal from '$lib/components/token/TokenManagementModal.svelte';
 
10
 
11
  interface Props {
12
  children?: import('svelte').Snippet;
@@ -14,8 +15,15 @@
14
 
15
  onMount(() => {
16
  fetchModels();
 
 
 
17
  });
18
 
 
 
 
 
19
  let { children }: Props = $props();
20
  </script>
21
 
 
7
  import { fetchModels, modelsState } from '$lib/state/models.svelte';
8
  import MainLoading from '$lib/components/loading/MainLoading.svelte';
9
  import TokenManagementModal from '$lib/components/token/TokenManagementModal.svelte';
10
+ import { breakpointsState } from '$lib/state/breakpoints.svelte';
11
 
12
  interface Props {
13
  children?: import('svelte').Snippet;
 
15
 
16
  onMount(() => {
17
  fetchModels();
18
+ handleBreakoints();
19
+ window.addEventListener('resize', handleBreakoints);
20
+ return () => window.removeEventListener('resize', handleBreakoints);
21
  });
22
 
23
+ function handleBreakoints() {
24
+ breakpointsState.isMobile = window.innerWidth < 768;
25
+ }
26
+
27
  let { children }: Props = $props();
28
  </script>
29
 
src/routes/+page.svelte CHANGED
@@ -6,11 +6,10 @@
6
  Panel,
7
  BackgroundVariant,
8
  type Node,
9
- type Edge,
10
- type ColorMode
11
  } from '@xyflow/svelte';
12
  import '@xyflow/svelte/dist/style.css';
13
- import { theme, mode } from 'mode-watcher';
14
 
15
  import { modelsState } from '$lib/state/models.svelte';
16
  import { tokenModalState } from '$lib/state/token-modal.svelte';
@@ -20,6 +19,8 @@
20
  import type { ChatModel } from '$lib/helpers/types';
21
  import FitViewOnResize from '$lib/components/flow/FitViewOnResize.svelte';
22
  import PanelRightActions from '$lib/components/flow/actions/PanelRightActions.svelte';
 
 
23
 
24
  const nodeTypes = {
25
  user: User,
@@ -33,7 +34,8 @@
33
  type: 'user',
34
  position: { x: 0, y: 0 },
35
  data: {
36
- selectedModels: modelsState.models.slice(0, 2) as ChatModel[]
 
37
  }
38
  }
39
  ];
@@ -65,11 +67,11 @@
65
  proOptions={{ hideAttribution: true }}
66
  fitViewOptions={{
67
  maxZoom: 1,
68
- minZoom: 0.8,
69
  interpolate: 'smooth',
70
  duration: 500
71
  }}
72
- defaultEdgeOptions={{ type: 'bezier' }}
73
  class="bg-background!"
74
  >
75
  <FitViewOnResize {initialNodes} />
@@ -84,7 +86,7 @@
84
  patternColor={mode.current === 'dark' ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)'}
85
  class="bg-background!"
86
  />
87
- <PanelRightActions />
88
  <Panel
89
  position="bottom-left"
90
  class="flex items-center justify-center gap-1.5 rounded-lg border border-border bg-background py-1.5 pr-3.5 pl-2.5 shadow-xs"
 
6
  Panel,
7
  BackgroundVariant,
8
  type Node,
9
+ type Edge
 
10
  } from '@xyflow/svelte';
11
  import '@xyflow/svelte/dist/style.css';
12
+ import { mode } from 'mode-watcher';
13
 
14
  import { modelsState } from '$lib/state/models.svelte';
15
  import { tokenModalState } from '$lib/state/token-modal.svelte';
 
19
  import type { ChatModel } from '$lib/helpers/types';
20
  import FitViewOnResize from '$lib/components/flow/FitViewOnResize.svelte';
21
  import PanelRightActions from '$lib/components/flow/actions/PanelRightActions.svelte';
22
+ import { MAX_DEFAULT_MODELS } from '$lib';
23
+ import { breakpointsState } from '$lib/state/breakpoints.svelte';
24
 
25
  const nodeTypes = {
26
  user: User,
 
34
  type: 'user',
35
  position: { x: 0, y: 0 },
36
  data: {
37
+ isFirstNode: true,
38
+ selectedModels: modelsState.models.slice(0, MAX_DEFAULT_MODELS) as ChatModel[]
39
  }
40
  }
41
  ];
 
67
  proOptions={{ hideAttribution: true }}
68
  fitViewOptions={{
69
  maxZoom: 1,
70
+ minZoom: breakpointsState.isMobile ? 1 : 0.8,
71
  interpolate: 'smooth',
72
  duration: 500
73
  }}
74
+ defaultEdgeOptions={{ type: 'smoothstep' }}
75
  class="bg-background!"
76
  >
77
  <FitViewOnResize {initialNodes} />
 
86
  patternColor={mode.current === 'dark' ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)'}
87
  class="bg-background!"
88
  />
89
+ <PanelRightActions canReset={nodes.length > 1} />
90
  <Panel
91
  position="bottom-left"
92
  class="flex items-center justify-center gap-1.5 rounded-lg border border-border bg-background py-1.5 pr-3.5 pl-2.5 shadow-xs"
src/routes/api/+server.ts CHANGED
@@ -4,9 +4,9 @@ import { InferenceClient } from '@huggingface/inference';
4
  import { env } from '$env/dynamic/private';
5
 
6
  export async function POST({ request }: RequestEvent) {
7
- const { model, prompt, messages, options, provider = 'auto' } = await request.json();
8
 
9
- if (!model || !prompt) {
10
  return json({ error: 'Model and prompt are required' }, { status: 400 });
11
  }
12
 
@@ -22,8 +22,7 @@ export async function POST({ request }: RequestEvent) {
22
  content:
23
  "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."
24
  },
25
- ...(messages ?? []),
26
- { role: 'user', content: prompt }
27
  ]
28
  },
29
  {
 
4
  import { env } from '$env/dynamic/private';
5
 
6
  export async function POST({ request }: RequestEvent) {
7
+ const { model, messages, options, provider = 'auto' } = await request.json();
8
 
9
+ if (!model) {
10
  return json({ error: 'Model and prompt are required' }, { status: 400 });
11
  }
12
 
 
22
  content:
23
  "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."
24
  },
25
+ ...(messages ?? [])
 
26
  ]
27
  },
28
  {