victor HF Staff commited on
Commit
32b18be
·
unverified ·
1 Parent(s): 97d716c

Prompt params (#1949)

Browse files

* Add support for 'prompt' URL param and draft binding

Introduces a utility to sanitize URL parameters and updates chat window logic to use a 'draft' state instead of 'message'. Both main and model-specific chat pages now support pre-filling the chat input from a 'prompt' URL parameter, and clear the parameter after use. This improves UX for deep-linking and sharing prompts.

* Sanitize URL parameters in model page

Added usage of sanitizeUrlParam for 'q' and 'prompt' URL parameters in the model page to prevent unsafe input. Wrapped parameter handling in a try-catch block to improve error handling and log failures.

src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -49,6 +49,7 @@
49
  onstop?: () => void;
50
  onretry?: (payload: { id: Message["id"]; content?: string }) => void;
51
  onshowAlternateMsg?: (payload: { id: Message["id"] }) => void;
 
52
  }
53
 
54
  let {
@@ -61,6 +62,7 @@
61
  models,
62
  preprompt = undefined,
63
  files = $bindable([]),
 
64
  onmessage,
65
  onstop,
66
  onretry,
@@ -69,15 +71,14 @@
69
 
70
  let isReadOnly = $derived(!models.some((model) => model.id === currentModel.id));
71
 
72
- let message: string = $state("");
73
  let shareModalOpen = $state(false);
74
  let editMsdgId: Message["id"] | null = $state(null);
75
  let pastedLongContent = $state(false);
76
 
77
  const handleSubmit = () => {
78
- if (loading) return;
79
- onmessage?.(message);
80
- message = "";
81
  };
82
 
83
  let lastTarget: EventTarget | null = null;
@@ -248,7 +249,7 @@
248
  );
249
  let routerUserMessages = $derived(messages.filter((msg) => msg.from === "user"));
250
  let shouldShowRouterFollowUps = $derived(
251
- !message.length &&
252
  activeRouterExamplePrompt &&
253
  routerFollowUps.length > 0 &&
254
  routerUserMessages.length === 1 &&
@@ -279,7 +280,7 @@
279
  $loginModalOpen = true;
280
  return;
281
  }
282
- message = prompt;
283
  handleSubmit();
284
  }
285
 
@@ -398,7 +399,7 @@
398
  dark:from-gray-900 dark:via-gray-900/100
399
  dark:to-gray-900/0 max-sm:py-0 sm:px-5 md:pb-4 xl:max-w-4xl [&>*]:pointer-events-auto"
400
  >
401
- {#if !message.length && !messages.length && !sources.length && !loading && currentModel.isRouter && routerExamples.length && !hideRouterExamples && !lastIsError}
402
  <div
403
  class="no-scrollbar mb-3 flex w-full select-none justify-start gap-2 overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500"
404
  >
@@ -485,7 +486,7 @@
485
  <ChatInput
486
  placeholder={isReadOnly ? "This conversation is read-only." : "Ask anything"}
487
  {loading}
488
- bind:value={message}
489
  bind:files
490
  mimeTypes={activeMimeTypes}
491
  onsubmit={handleSubmit}
@@ -504,11 +505,11 @@
504
  />
505
  {:else}
506
  <button
507
- class="btn absolute bottom-2 right-2 size-7 self-end rounded-full border bg-white text-black shadow transition-none enabled:hover:bg-white enabled:hover:shadow-inner dark:border-transparent dark:bg-gray-600 dark:text-white dark:hover:enabled:bg-black {!message ||
508
  isReadOnly
509
  ? ''
510
  : '!bg-black !text-white dark:!bg-white dark:!text-black'}"
511
- disabled={!message || isReadOnly}
512
  type="submit"
513
  aria-label="Send message"
514
  name="submit"
 
49
  onstop?: () => void;
50
  onretry?: (payload: { id: Message["id"]; content?: string }) => void;
51
  onshowAlternateMsg?: (payload: { id: Message["id"] }) => void;
52
+ draft?: string;
53
  }
54
 
55
  let {
 
62
  models,
63
  preprompt = undefined,
64
  files = $bindable([]),
65
+ draft = $bindable(""),
66
  onmessage,
67
  onstop,
68
  onretry,
 
71
 
72
  let isReadOnly = $derived(!models.some((model) => model.id === currentModel.id));
73
 
 
74
  let shareModalOpen = $state(false);
75
  let editMsdgId: Message["id"] | null = $state(null);
76
  let pastedLongContent = $state(false);
77
 
78
  const handleSubmit = () => {
79
+ if (loading || !draft) return;
80
+ onmessage?.(draft);
81
+ draft = "";
82
  };
83
 
84
  let lastTarget: EventTarget | null = null;
 
249
  );
250
  let routerUserMessages = $derived(messages.filter((msg) => msg.from === "user"));
251
  let shouldShowRouterFollowUps = $derived(
252
+ !draft.length &&
253
  activeRouterExamplePrompt &&
254
  routerFollowUps.length > 0 &&
255
  routerUserMessages.length === 1 &&
 
280
  $loginModalOpen = true;
281
  return;
282
  }
283
+ draft = prompt;
284
  handleSubmit();
285
  }
286
 
 
399
  dark:from-gray-900 dark:via-gray-900/100
400
  dark:to-gray-900/0 max-sm:py-0 sm:px-5 md:pb-4 xl:max-w-4xl [&>*]:pointer-events-auto"
401
  >
402
+ {#if !draft.length && !messages.length && !sources.length && !loading && currentModel.isRouter && routerExamples.length && !hideRouterExamples && !lastIsError}
403
  <div
404
  class="no-scrollbar mb-3 flex w-full select-none justify-start gap-2 overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500"
405
  >
 
486
  <ChatInput
487
  placeholder={isReadOnly ? "This conversation is read-only." : "Ask anything"}
488
  {loading}
489
+ bind:value={draft}
490
  bind:files
491
  mimeTypes={activeMimeTypes}
492
  onsubmit={handleSubmit}
 
505
  />
506
  {:else}
507
  <button
508
+ class="btn absolute bottom-2 right-2 size-7 self-end rounded-full border bg-white text-black shadow transition-none enabled:hover:bg-white enabled:hover:shadow-inner dark:border-transparent dark:bg-gray-600 dark:text-white dark:hover:enabled:bg-black {!draft ||
509
  isReadOnly
510
  ? ''
511
  : '!bg-black !text-white dark:!bg-white dark:!text-black'}"
512
+ disabled={!draft || isReadOnly}
513
  type="submit"
514
  aria-label="Send message"
515
  name="submit"
src/lib/utils/urlParams.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const MAX_PARAM_LENGTH = 10_000;
2
+
3
+ export function sanitizeUrlParam(value: string | null): string | null {
4
+ if (value == null) return null;
5
+
6
+ const trimmed = value.trim();
7
+ if (!trimmed.length) return null;
8
+ if (trimmed.length > MAX_PARAM_LENGTH) return null;
9
+
10
+ return trimmed;
11
+ }
12
+
13
+ export { MAX_PARAM_LENGTH };
src/routes/+page.svelte CHANGED
@@ -7,17 +7,19 @@
7
  const publicConfig = usePublicConfig();
8
 
9
  import ChatWindow from "$lib/components/chat/ChatWindow.svelte";
10
- import { ERROR_MESSAGES, error } from "$lib/stores/errors";
11
- import { pendingMessage } from "$lib/stores/pendingMessage";
12
- import { useSettingsStore } from "$lib/stores/settings.js";
13
- import { findCurrentModel } from "$lib/utils/models";
14
- import { onMount } from "svelte";
 
15
 
16
  let { data } = $props();
17
 
18
  let hasModels = $derived(Boolean(data.models?.length));
19
  let loading = $state(false);
20
  let files: File[] = $state([]);
 
21
 
22
  const settings = useSettingsStore();
23
 
@@ -74,9 +76,26 @@
74
  }
75
 
76
  onMount(() => {
77
- // check if there's a ?q query param with a message
78
- const query = page.url.searchParams.get("q");
79
- if (query) createConversation(query);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  });
81
 
82
  let currentModel = $derived(findCurrentModel(data.models, data.oldModels, $settings.activeModel));
@@ -93,6 +112,7 @@
93
  {currentModel}
94
  models={data.models}
95
  bind:files
 
96
  />
97
  {:else}
98
  <div class="mx-auto my-20 max-w-xl rounded-xl border p-6 text-center dark:border-gray-700">
 
7
  const publicConfig = usePublicConfig();
8
 
9
  import ChatWindow from "$lib/components/chat/ChatWindow.svelte";
10
+ import { ERROR_MESSAGES, error } from "$lib/stores/errors";
11
+ import { pendingMessage } from "$lib/stores/pendingMessage";
12
+ import { useSettingsStore } from "$lib/stores/settings.js";
13
+ import { findCurrentModel } from "$lib/utils/models";
14
+ import { sanitizeUrlParam } from "$lib/utils/urlParams";
15
+ import { onMount } from "svelte";
16
 
17
  let { data } = $props();
18
 
19
  let hasModels = $derived(Boolean(data.models?.length));
20
  let loading = $state(false);
21
  let files: File[] = $state([]);
22
+ let draft = $state("");
23
 
24
  const settings = useSettingsStore();
25
 
 
76
  }
77
 
78
  onMount(() => {
79
+ try {
80
+ const query = sanitizeUrlParam(page.url.searchParams.get("q"));
81
+ if (query) {
82
+ void createConversation(query);
83
+ const url = new URL(page.url);
84
+ url.searchParams.delete("q");
85
+ history.replaceState({}, "", url);
86
+ return;
87
+ }
88
+
89
+ const promptQuery = sanitizeUrlParam(page.url.searchParams.get("prompt"));
90
+ if (promptQuery && !draft) {
91
+ draft = promptQuery;
92
+ const url = new URL(page.url);
93
+ url.searchParams.delete("prompt");
94
+ history.replaceState({}, "", url);
95
+ }
96
+ } catch (err) {
97
+ console.error("Failed to process URL parameters:", err);
98
+ }
99
  });
100
 
101
  let currentModel = $derived(findCurrentModel(data.models, data.oldModels, $settings.activeModel));
 
112
  {currentModel}
113
  models={data.models}
114
  bind:files
115
+ bind:draft
116
  />
117
  {:else}
118
  <div class="mx-auto my-20 max-w-xl rounded-xl border p-6 text-center dark:border-gray-700">
src/routes/models/[...model]/+page.svelte CHANGED
@@ -10,11 +10,13 @@
10
  import { useSettingsStore } from "$lib/stores/settings";
11
  import { ERROR_MESSAGES, error } from "$lib/stores/errors";
12
  import { pendingMessage } from "$lib/stores/pendingMessage";
 
13
 
14
  let { data } = $props();
15
 
16
  let loading = $state(false);
17
  let files: File[] = $state([]);
 
18
 
19
  const settings = useSettingsStore();
20
  const modelId = page.params.model;
@@ -59,9 +61,27 @@
59
  }
60
  }
61
 
62
- onMount(async () => {
63
- const query = page.url.searchParams.get("q");
64
- if (query) createConversation(query);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  settings.instantSet({ activeModel: modelId });
67
  });
@@ -85,4 +105,5 @@
85
  currentModel={findCurrentModel(data.models, data.oldModels, modelId)}
86
  models={data.models}
87
  bind:files
 
88
  />
 
10
  import { useSettingsStore } from "$lib/stores/settings";
11
  import { ERROR_MESSAGES, error } from "$lib/stores/errors";
12
  import { pendingMessage } from "$lib/stores/pendingMessage";
13
+ import { sanitizeUrlParam } from "$lib/utils/urlParams";
14
 
15
  let { data } = $props();
16
 
17
  let loading = $state(false);
18
  let files: File[] = $state([]);
19
+ let draft = $state("");
20
 
21
  const settings = useSettingsStore();
22
  const modelId = page.params.model;
 
61
  }
62
  }
63
 
64
+ onMount(() => {
65
+ try {
66
+ const query = sanitizeUrlParam(page.url.searchParams.get("q"));
67
+ if (query) {
68
+ void createConversation(query);
69
+ const url = new URL(page.url);
70
+ url.searchParams.delete("q");
71
+ history.replaceState({}, "", url);
72
+ return;
73
+ }
74
+
75
+ const promptQuery = sanitizeUrlParam(page.url.searchParams.get("prompt"));
76
+ if (promptQuery && !draft) {
77
+ draft = promptQuery;
78
+ const url = new URL(page.url);
79
+ url.searchParams.delete("prompt");
80
+ history.replaceState({}, "", url);
81
+ }
82
+ } catch (err) {
83
+ console.error("Failed to process URL parameters:", err);
84
+ }
85
 
86
  settings.instantSet({ activeModel: modelId });
87
  });
 
105
  currentModel={findCurrentModel(data.models, data.oldModels, modelId)}
106
  models={data.models}
107
  bind:files
108
+ bind:draft
109
  />