atrokhym commited on
Commit
4b236e9
·
2 Parent(s): 32f758b fe80e5f

Merge remote-tracking branch 'upstream/main'

Browse files
.env.example CHANGED
@@ -5,6 +5,12 @@
5
  # You only need this environment variable set if you want to use Groq models
6
  GROQ_API_KEY=
7
 
 
 
 
 
 
 
8
  # Get your Open AI API Key by following these instructions -
9
  # https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
10
  # You only need this environment variable set if you want to use GPT models
@@ -55,4 +61,4 @@ LMSTUDIO_API_BASE_URL=
55
  XAI_API_KEY=
56
 
57
  # Include this environment variable if you want more logging for debugging locally
58
- VITE_LOG_LEVEL=debug
 
5
  # You only need this environment variable set if you want to use Groq models
6
  GROQ_API_KEY=
7
 
8
+ # Get your HuggingFace API Key here -
9
+ # https://huggingface.co/settings/tokens
10
+ # You only need this environment variable set if you want to use HuggingFace models
11
+ HuggingFace_API_KEY=
12
+
13
+
14
  # Get your Open AI API Key by following these instructions -
15
  # https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
16
  # You only need this environment variable set if you want to use GPT models
 
61
  XAI_API_KEY=
62
 
63
  # Include this environment variable if you want more logging for debugging locally
64
+ VITE_LOG_LEVEL=debug
.husky/commit-msg DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env sh
2
-
3
- . "$(dirname "$0")/_/husky.sh"
4
-
5
- npx commitlint --edit $1
6
-
7
- exit 0
 
 
 
 
 
 
 
 
CONTRIBUTING.md CHANGED
@@ -72,6 +72,7 @@ pnpm install
72
  - Add your LLM API keys (only set the ones you plan to use):
73
  ```bash
74
  GROQ_API_KEY=XXX
 
75
  OPENAI_API_KEY=XXX
76
  ANTHROPIC_API_KEY=XXX
77
  ...
 
72
  - Add your LLM API keys (only set the ones you plan to use):
73
  ```bash
74
  GROQ_API_KEY=XXX
75
+ HuggingFace_API_KEY=XXX
76
  OPENAI_API_KEY=XXX
77
  ANTHROPIC_API_KEY=XXX
78
  ...
Dockerfile CHANGED
@@ -19,6 +19,7 @@ FROM base AS bolt-ai-production
19
 
20
  # Define environment variables with default values or let them be overridden
21
  ARG GROQ_API_KEY
 
22
  ARG OPENAI_API_KEY
23
  ARG ANTHROPIC_API_KEY
24
  ARG OPEN_ROUTER_API_KEY
@@ -28,6 +29,7 @@ ARG VITE_LOG_LEVEL=debug
28
 
29
  ENV WRANGLER_SEND_METRICS=false \
30
  GROQ_API_KEY=${GROQ_API_KEY} \
 
31
  OPENAI_API_KEY=${OPENAI_API_KEY} \
32
  ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
33
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
@@ -48,6 +50,7 @@ FROM base AS bolt-ai-development
48
 
49
  # Define the same environment variables for development
50
  ARG GROQ_API_KEY
 
51
  ARG OPENAI_API_KEY
52
  ARG ANTHROPIC_API_KEY
53
  ARG OPEN_ROUTER_API_KEY
@@ -56,6 +59,7 @@ ARG OLLAMA_API_BASE_URL
56
  ARG VITE_LOG_LEVEL=debug
57
 
58
  ENV GROQ_API_KEY=${GROQ_API_KEY} \
 
59
  OPENAI_API_KEY=${OPENAI_API_KEY} \
60
  ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
61
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
 
19
 
20
  # Define environment variables with default values or let them be overridden
21
  ARG GROQ_API_KEY
22
+ ARG HuggingFace_API_KEY
23
  ARG OPENAI_API_KEY
24
  ARG ANTHROPIC_API_KEY
25
  ARG OPEN_ROUTER_API_KEY
 
29
 
30
  ENV WRANGLER_SEND_METRICS=false \
31
  GROQ_API_KEY=${GROQ_API_KEY} \
32
+ HuggingFace_KEY=${HuggingFace_API_KEY} \
33
  OPENAI_API_KEY=${OPENAI_API_KEY} \
34
  ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
35
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
 
50
 
51
  # Define the same environment variables for development
52
  ARG GROQ_API_KEY
53
+ ARG HuggingFace
54
  ARG OPENAI_API_KEY
55
  ARG ANTHROPIC_API_KEY
56
  ARG OPEN_ROUTER_API_KEY
 
59
  ARG VITE_LOG_LEVEL=debug
60
 
61
  ENV GROQ_API_KEY=${GROQ_API_KEY} \
62
+ HuggingFace_API_KEY=${HuggingFace_API_KEY} \
63
  OPENAI_API_KEY=${OPENAI_API_KEY} \
64
  ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
65
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
app/components/chat/Artifact.tsx CHANGED
@@ -7,6 +7,7 @@ import type { ActionState } from '~/lib/runtime/action-runner';
7
  import { workbenchStore } from '~/lib/stores/workbench';
8
  import { classNames } from '~/utils/classNames';
9
  import { cubicEasingFn } from '~/utils/easings';
 
10
 
11
  const highlighterOptions = {
12
  langs: ['shell'],
@@ -129,6 +130,14 @@ const actionVariants = {
129
  visible: { opacity: 1, y: 0 },
130
  };
131
 
 
 
 
 
 
 
 
 
132
  const ActionList = memo(({ actions }: ActionListProps) => {
133
  return (
134
  <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
@@ -169,7 +178,10 @@ const ActionList = memo(({ actions }: ActionListProps) => {
169
  {type === 'file' ? (
170
  <div>
171
  Create{' '}
172
- <code className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md">
 
 
 
173
  {action.filePath}
174
  </code>
175
  </div>
 
7
  import { workbenchStore } from '~/lib/stores/workbench';
8
  import { classNames } from '~/utils/classNames';
9
  import { cubicEasingFn } from '~/utils/easings';
10
+ import { WORK_DIR } from '~/utils/constants';
11
 
12
  const highlighterOptions = {
13
  langs: ['shell'],
 
130
  visible: { opacity: 1, y: 0 },
131
  };
132
 
133
+ function openArtifactInWorkbench(filePath: any) {
134
+ if (workbenchStore.currentView.get() !== 'code') {
135
+ workbenchStore.currentView.set('code');
136
+ }
137
+
138
+ workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`);
139
+ }
140
+
141
  const ActionList = memo(({ actions }: ActionListProps) => {
142
  return (
143
  <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
 
178
  {type === 'file' ? (
179
  <div>
180
  Create{' '}
181
+ <code
182
+ className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
183
+ onClick={() => openArtifactInWorkbench(action.filePath)}
184
+ >
185
  {action.filePath}
186
  </code>
187
  </div>
app/components/chat/Messages.client.tsx CHANGED
@@ -3,6 +3,11 @@ import React from 'react';
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
 
 
 
 
 
6
 
7
  interface MessagesProps {
8
  id?: string;
@@ -13,41 +18,112 @@ interface MessagesProps {
13
 
14
  export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
15
  const { id, isStreaming = false, messages = [] } = props;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  return (
18
- <div id={id} ref={ref} className={props.className}>
19
- {messages.length > 0
20
- ? messages.map((message, index) => {
21
- const { role, content } = message;
22
- const isUserMessage = role === 'user';
23
- const isFirst = index === 0;
24
- const isLast = index === messages.length - 1;
25
-
26
- return (
27
- <div
28
- key={index}
29
- className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
30
- 'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
31
- 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
32
- isStreaming && isLast,
33
- 'mt-4': !isFirst,
34
- })}
35
- >
36
- {isUserMessage && (
37
- <div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
38
- <div className="i-ph:user-fill text-xl"></div>
 
 
 
 
 
39
  </div>
40
- )}
41
- <div className="grid grid-col-1 w-full">
42
- {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </div>
44
- </div>
45
- );
46
- })
47
- : null}
48
- {isStreaming && (
49
- <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
50
- )}
51
- </div>
52
  );
53
  });
 
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
6
+ import * as Tooltip from '@radix-ui/react-tooltip';
7
+ import { useLocation, useNavigate } from '@remix-run/react';
8
+ import { db, chatId } from '~/lib/persistence/useChatHistory';
9
+ import { forkChat } from '~/lib/persistence/db';
10
+ import { toast } from 'react-toastify';
11
 
12
  interface MessagesProps {
13
  id?: string;
 
18
 
19
  export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
20
  const { id, isStreaming = false, messages = [] } = props;
21
+ const location = useLocation();
22
+ const navigate = useNavigate();
23
+
24
+ const handleRewind = (messageId: string) => {
25
+ const searchParams = new URLSearchParams(location.search);
26
+ searchParams.set('rewindTo', messageId);
27
+ window.location.search = searchParams.toString();
28
+ };
29
+
30
+ const handleFork = async (messageId: string) => {
31
+ try {
32
+ if (!db || !chatId.get()) {
33
+ toast.error('Chat persistence is not available');
34
+ return;
35
+ }
36
+
37
+ const urlId = await forkChat(db, chatId.get()!, messageId);
38
+ window.location.href = `/chat/${urlId}`;
39
+ } catch (error) {
40
+ toast.error('Failed to fork chat: ' + (error as Error).message);
41
+ }
42
+ };
43
 
44
  return (
45
+ <Tooltip.Provider delayDuration={200}>
46
+ <div id={id} ref={ref} className={props.className}>
47
+ {messages.length > 0
48
+ ? messages.map((message, index) => {
49
+ const { role, content, id: messageId } = message;
50
+ const isUserMessage = role === 'user';
51
+ const isFirst = index === 0;
52
+ const isLast = index === messages.length - 1;
53
+
54
+ return (
55
+ <div
56
+ key={index}
57
+ className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
58
+ 'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
59
+ 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
60
+ isStreaming && isLast,
61
+ 'mt-4': !isFirst,
62
+ })}
63
+ >
64
+ {isUserMessage && (
65
+ <div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
66
+ <div className="i-ph:user-fill text-xl"></div>
67
+ </div>
68
+ )}
69
+ <div className="grid grid-col-1 w-full">
70
+ {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
71
  </div>
72
+ {!isUserMessage && (<div className="flex gap-2">
73
+ <Tooltip.Root>
74
+ <Tooltip.Trigger asChild>
75
+ {messageId && (<button
76
+ onClick={() => handleRewind(messageId)}
77
+ key='i-ph:arrow-u-up-left'
78
+ className={classNames(
79
+ 'i-ph:arrow-u-up-left',
80
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
81
+ )}
82
+ />)}
83
+ </Tooltip.Trigger>
84
+ <Tooltip.Portal>
85
+ <Tooltip.Content
86
+ className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
87
+ sideOffset={5}
88
+ style={{zIndex: 1000}}
89
+ >
90
+ Revert to this message
91
+ <Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
92
+ </Tooltip.Content>
93
+ </Tooltip.Portal>
94
+ </Tooltip.Root>
95
+
96
+ <Tooltip.Root>
97
+ <Tooltip.Trigger asChild>
98
+ <button
99
+ onClick={() => handleFork(messageId)}
100
+ key='i-ph:git-fork'
101
+ className={classNames(
102
+ 'i-ph:git-fork',
103
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
104
+ )}
105
+ />
106
+ </Tooltip.Trigger>
107
+ <Tooltip.Portal>
108
+ <Tooltip.Content
109
+ className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
110
+ sideOffset={5}
111
+ style={{zIndex: 1000}}
112
+ >
113
+ Fork chat from this message
114
+ <Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
115
+ </Tooltip.Content>
116
+ </Tooltip.Portal>
117
+ </Tooltip.Root>
118
+ </div>)}
119
  </div>
120
+ );
121
+ })
122
+ : null}
123
+ {isStreaming && (
124
+ <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
125
+ )}
126
+ </div>
127
+ </Tooltip.Provider>
128
  );
129
  });
app/components/sidebar/HistoryItem.tsx CHANGED
@@ -5,9 +5,10 @@ import { type ChatHistoryItem } from '~/lib/persistence';
5
  interface HistoryItemProps {
6
  item: ChatHistoryItem;
7
  onDelete?: (event: React.UIEvent) => void;
 
8
  }
9
 
10
- export function HistoryItem({ item, onDelete }: HistoryItemProps) {
11
  const [hovering, setHovering] = useState(false);
12
  const hoverRef = useRef<HTMLDivElement>(null);
13
 
@@ -44,7 +45,14 @@ export function HistoryItem({ item, onDelete }: HistoryItemProps) {
44
  {item.description}
45
  <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
46
  {hovering && (
47
- <div className="flex items-center p-1 text-bolt-elements-textSecondary hover:text-bolt-elements-item-contentDanger">
 
 
 
 
 
 
 
48
  <Dialog.Trigger asChild>
49
  <button
50
  className="i-ph:trash scale-110"
 
5
  interface HistoryItemProps {
6
  item: ChatHistoryItem;
7
  onDelete?: (event: React.UIEvent) => void;
8
+ onDuplicate?: (id: string) => void;
9
  }
10
 
11
+ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
12
  const [hovering, setHovering] = useState(false);
13
  const hoverRef = useRef<HTMLDivElement>(null);
14
 
 
45
  {item.description}
46
  <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
47
  {hovering && (
48
+ <div className="flex items-center p-1 text-bolt-elements-textSecondary">
49
+ {onDuplicate && (
50
+ <button
51
+ className="i-ph:copy scale-110 mr-2"
52
+ onClick={() => onDuplicate?.(item.id)}
53
+ title="Duplicate chat"
54
+ />
55
+ )}
56
  <Dialog.Trigger asChild>
57
  <button
58
  className="i-ph:trash scale-110"
app/components/sidebar/Menu.client.tsx CHANGED
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { IconButton } from '~/components/ui/IconButton';
6
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
7
- import { db, deleteById, getAll, chatId, type ChatHistoryItem } from '~/lib/persistence';
8
  import { cubicEasingFn } from '~/utils/easings';
9
  import { logger } from '~/utils/logger';
10
  import { HistoryItem } from './HistoryItem';
@@ -34,6 +34,7 @@ const menuVariants = {
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
  export const Menu = () => {
 
37
  const menuRef = useRef<HTMLDivElement>(null);
38
  const [list, setList] = useState<ChatHistoryItem[]>([]);
39
  const [open, setOpen] = useState(false);
@@ -99,6 +100,17 @@ export const Menu = () => {
99
  };
100
  }, []);
101
 
 
 
 
 
 
 
 
 
 
 
 
102
  return (
103
  <motion.div
104
  ref={menuRef}
@@ -128,7 +140,12 @@ export const Menu = () => {
128
  {category}
129
  </div>
130
  {items.map((item) => (
131
- <HistoryItem key={item.id} item={item} onDelete={() => setDialogContent({ type: 'delete', item })} />
 
 
 
 
 
132
  ))}
133
  </div>
134
  ))}
 
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { IconButton } from '~/components/ui/IconButton';
6
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
7
+ import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
8
  import { cubicEasingFn } from '~/utils/easings';
9
  import { logger } from '~/utils/logger';
10
  import { HistoryItem } from './HistoryItem';
 
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
  export const Menu = () => {
37
+ const { duplicateCurrentChat } = useChatHistory();
38
  const menuRef = useRef<HTMLDivElement>(null);
39
  const [list, setList] = useState<ChatHistoryItem[]>([]);
40
  const [open, setOpen] = useState(false);
 
100
  };
101
  }, []);
102
 
103
+ const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
104
+ event.preventDefault();
105
+
106
+ setDialogContent({ type: 'delete', item });
107
+ };
108
+
109
+ const handleDuplicate = async (id: string) => {
110
+ await duplicateCurrentChat(id);
111
+ loadEntries(); // Reload the list after duplication
112
+ };
113
+
114
  return (
115
  <motion.div
116
  ref={menuRef}
 
140
  {category}
141
  </div>
142
  {items.map((item) => (
143
+ <HistoryItem
144
+ key={item.id}
145
+ item={item}
146
+ onDelete={(event) => handleDeleteClick(event, item)}
147
+ onDuplicate={() => handleDuplicate(item.id)}
148
+ />
149
  ))}
150
  </div>
151
  ))}
app/lib/.server/llm/api-key.ts CHANGED
@@ -23,6 +23,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
23
  return env.GOOGLE_GENERATIVE_AI_API_KEY || cloudflareEnv.GOOGLE_GENERATIVE_AI_API_KEY;
24
  case 'Groq':
25
  return env.GROQ_API_KEY || cloudflareEnv.GROQ_API_KEY;
 
 
26
  case 'OpenRouter':
27
  return env.OPEN_ROUTER_API_KEY || cloudflareEnv.OPEN_ROUTER_API_KEY;
28
  case 'Deepseek':
 
23
  return env.GOOGLE_GENERATIVE_AI_API_KEY || cloudflareEnv.GOOGLE_GENERATIVE_AI_API_KEY;
24
  case 'Groq':
25
  return env.GROQ_API_KEY || cloudflareEnv.GROQ_API_KEY;
26
+ case 'HuggingFace':
27
+ return env.HuggingFace_API_KEY || cloudflareEnv.HuggingFace_API_KEY;
28
  case 'OpenRouter':
29
  return env.OPEN_ROUTER_API_KEY || cloudflareEnv.OPEN_ROUTER_API_KEY;
30
  case 'Deepseek':
app/lib/.server/llm/model.ts CHANGED
@@ -56,6 +56,15 @@ export function getGroqModel(apiKey: string, model: string) {
56
  return openai(model);
57
  }
58
 
 
 
 
 
 
 
 
 
 
59
  export function getOllamaModel(baseURL: string, model: string) {
60
  let Ollama = ollama(model, {
61
  numCtx: 32768,
@@ -110,6 +119,8 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re
110
  return getOpenAIModel(apiKey, model);
111
  case 'Groq':
112
  return getGroqModel(apiKey, model);
 
 
113
  case 'OpenRouter':
114
  return getOpenRouterModel(apiKey, model);
115
  case 'Google':
 
56
  return openai(model);
57
  }
58
 
59
+ export function getHuggingFaceModel(apiKey: string, model: string) {
60
+ const openai = createOpenAI({
61
+ baseURL: 'https://api-inference.huggingface.co/v1/',
62
+ apiKey,
63
+ });
64
+
65
+ return openai(model);
66
+ }
67
+
68
  export function getOllamaModel(baseURL: string, model: string) {
69
  let Ollama = ollama(model, {
70
  numCtx: 32768,
 
119
  return getOpenAIModel(apiKey, model);
120
  case 'Groq':
121
  return getGroqModel(apiKey, model);
122
+ case 'HuggingFace':
123
+ return getHuggingFaceModel(apiKey, model);
124
  case 'OpenRouter':
125
  return getOpenRouterModel(apiKey, model);
126
  case 'Google':
app/lib/.server/llm/prompts.ts CHANGED
@@ -88,7 +88,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
88
  Example:
89
 
90
  <${MODIFICATIONS_TAG_NAME}>
91
- <diff path="/home/project/src/main.js">
92
  @@ -2,7 +2,10 @@
93
  return a + b;
94
  }
@@ -103,7 +103,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
103
  +
104
  +console.log('The End');
105
  </diff>
106
- <file path="/home/project/package.json">
107
  // full file content here
108
  </file>
109
  </${MODIFICATIONS_TAG_NAME}>
 
88
  Example:
89
 
90
  <${MODIFICATIONS_TAG_NAME}>
91
+ <diff path="${WORK_DIR}/src/main.js">
92
  @@ -2,7 +2,10 @@
93
  return a + b;
94
  }
 
103
  +
104
  +console.log('The End');
105
  </diff>
106
+ <file path="${WORK_DIR}/package.json">
107
  // full file content here
108
  </file>
109
  </${MODIFICATIONS_TAG_NAME}>
app/lib/persistence/db.ts CHANGED
@@ -158,3 +158,50 @@ async function getUrlIds(db: IDBDatabase): Promise<string[]> {
158
  };
159
  });
160
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  };
159
  });
160
  }
161
+
162
+ export async function forkChat(db: IDBDatabase, chatId: string, messageId: string): Promise<string> {
163
+ const chat = await getMessages(db, chatId);
164
+ if (!chat) throw new Error('Chat not found');
165
+
166
+ // Find the index of the message to fork at
167
+ const messageIndex = chat.messages.findIndex(msg => msg.id === messageId);
168
+ if (messageIndex === -1) throw new Error('Message not found');
169
+
170
+ // Get messages up to and including the selected message
171
+ const messages = chat.messages.slice(0, messageIndex + 1);
172
+
173
+ // Generate new IDs
174
+ const newId = await getNextId(db);
175
+ const urlId = await getUrlId(db, newId);
176
+
177
+ // Create the forked chat
178
+ await setMessages(
179
+ db,
180
+ newId,
181
+ messages,
182
+ urlId,
183
+ chat.description ? `${chat.description} (fork)` : 'Forked chat'
184
+ );
185
+
186
+ return urlId;
187
+ }
188
+
189
+ export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
190
+ const chat = await getMessages(db, id);
191
+ if (!chat) {
192
+ throw new Error('Chat not found');
193
+ }
194
+
195
+ const newId = await getNextId(db);
196
+ const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
197
+
198
+ await setMessages(
199
+ db,
200
+ newId,
201
+ chat.messages,
202
+ newUrlId, // Use the new urlId
203
+ `${chat.description || 'Chat'} (copy)`
204
+ );
205
+
206
+ return newUrlId; // Return the urlId instead of id for navigation
207
+ }
app/lib/persistence/useChatHistory.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { useLoaderData, useNavigate } from '@remix-run/react';
2
  import { useState, useEffect } from 'react';
3
  import { atom } from 'nanostores';
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
7
- import { getMessages, getNextId, getUrlId, openDatabase, setMessages } from './db';
8
 
9
  export interface ChatHistoryItem {
10
  id: string;
@@ -24,6 +24,7 @@ export const description = atom<string | undefined>(undefined);
24
  export function useChatHistory() {
25
  const navigate = useNavigate();
26
  const { id: mixedId } = useLoaderData<{ id?: string }>();
 
27
 
28
  const [initialMessages, setInitialMessages] = useState<Message[]>([]);
29
  const [ready, setReady] = useState<boolean>(false);
@@ -44,7 +45,12 @@ export function useChatHistory() {
44
  getMessages(db, mixedId)
45
  .then((storedMessages) => {
46
  if (storedMessages && storedMessages.messages.length > 0) {
47
- setInitialMessages(storedMessages.messages);
 
 
 
 
 
48
  setUrlId(storedMessages.urlId);
49
  description.set(storedMessages.description);
50
  chatId.set(storedMessages.id);
@@ -93,6 +99,19 @@ export function useChatHistory() {
93
 
94
  await setMessages(db, chatId.get() as string, messages, urlId, description.get());
95
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  };
97
  }
98
 
 
1
+ import { useLoaderData, useNavigate, useSearchParams } from '@remix-run/react';
2
  import { useState, useEffect } from 'react';
3
  import { atom } from 'nanostores';
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
7
+ import { getMessages, getNextId, getUrlId, openDatabase, setMessages, duplicateChat } from './db';
8
 
9
  export interface ChatHistoryItem {
10
  id: string;
 
24
  export function useChatHistory() {
25
  const navigate = useNavigate();
26
  const { id: mixedId } = useLoaderData<{ id?: string }>();
27
+ const [searchParams] = useSearchParams();
28
 
29
  const [initialMessages, setInitialMessages] = useState<Message[]>([]);
30
  const [ready, setReady] = useState<boolean>(false);
 
45
  getMessages(db, mixedId)
46
  .then((storedMessages) => {
47
  if (storedMessages && storedMessages.messages.length > 0) {
48
+ const rewindId = searchParams.get('rewindTo');
49
+ const filteredMessages = rewindId
50
+ ? storedMessages.messages.slice(0, storedMessages.messages.findIndex((m) => m.id === rewindId) + 1)
51
+ : storedMessages.messages;
52
+
53
+ setInitialMessages(filteredMessages);
54
  setUrlId(storedMessages.urlId);
55
  description.set(storedMessages.description);
56
  chatId.set(storedMessages.id);
 
99
 
100
  await setMessages(db, chatId.get() as string, messages, urlId, description.get());
101
  },
102
+ duplicateCurrentChat: async (listItemId:string) => {
103
+ if (!db || (!mixedId && !listItemId)) {
104
+ return;
105
+ }
106
+
107
+ try {
108
+ const newId = await duplicateChat(db, mixedId || listItemId);
109
+ navigate(`/chat/${newId}`);
110
+ toast.success('Chat duplicated successfully');
111
+ } catch (error) {
112
+ toast.error('Failed to duplicate chat');
113
+ }
114
+ }
115
  };
116
  }
117
 
app/lib/runtime/action-runner.ts CHANGED
@@ -94,7 +94,7 @@ export class ActionRunner {
94
 
95
  this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
96
 
97
- this.#currentExecutionPromise = this.#currentExecutionPromise
98
  .then(() => {
99
  return this.#executeAction(actionId, isStreaming);
100
  })
@@ -119,7 +119,14 @@ export class ActionRunner {
119
  break;
120
  }
121
  case 'start': {
122
- await this.#runStartAction(action)
 
 
 
 
 
 
 
123
  break;
124
  }
125
  }
 
94
 
95
  this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
96
 
97
+ return this.#currentExecutionPromise = this.#currentExecutionPromise
98
  .then(() => {
99
  return this.#executeAction(actionId, isStreaming);
100
  })
 
119
  break;
120
  }
121
  case 'start': {
122
+ // making the start app non blocking
123
+
124
+ this.#runStartAction(action).then(()=>this.#updateAction(actionId, { status: 'complete' }))
125
+ .catch(()=>this.#updateAction(actionId, { status: 'failed', error: 'Action failed' }))
126
+ // adding a delay to avoid any race condition between 2 start actions
127
+ // i am up for a better approch
128
+ await new Promise(resolve=>setTimeout(resolve,2000))
129
+ return
130
  break;
131
  }
132
  }
app/lib/stores/workbench.ts CHANGED
@@ -14,6 +14,7 @@ import { saveAs } from 'file-saver';
14
  import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
15
  import * as nodePath from 'node:path';
16
  import type { WebContainerProcess } from '@webcontainer/api';
 
17
 
18
  export interface ArtifactState {
19
  id: string;
@@ -42,7 +43,7 @@ export class WorkbenchStore {
42
  modifiedFiles = new Set<string>();
43
  artifactIdList: string[] = [];
44
  #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined;
45
-
46
  constructor() {
47
  if (import.meta.hot) {
48
  import.meta.hot.data.artifacts = this.artifacts;
@@ -52,6 +53,10 @@ export class WorkbenchStore {
52
  }
53
  }
54
 
 
 
 
 
55
  get previews() {
56
  return this.#previewsStore.previews;
57
  }
@@ -255,8 +260,11 @@ export class WorkbenchStore {
255
 
256
  this.artifacts.setKey(messageId, { ...artifact, ...state });
257
  }
258
-
259
- async addAction(data: ActionCallbackData) {
 
 
 
260
  const { messageId } = data;
261
 
262
  const artifact = this.#getArtifact(messageId);
@@ -265,10 +273,18 @@ export class WorkbenchStore {
265
  unreachable('Artifact not found');
266
  }
267
 
268
- artifact.runner.addAction(data);
269
  }
270
 
271
- async runAction(data: ActionCallbackData, isStreaming: boolean = false) {
 
 
 
 
 
 
 
 
272
  const { messageId } = data;
273
 
274
  const artifact = this.#getArtifact(messageId);
@@ -293,11 +309,11 @@ export class WorkbenchStore {
293
  this.#editorStore.updateFile(fullPath, data.action.content);
294
 
295
  if (!isStreaming) {
296
- this.resetCurrentDocument();
297
  await artifact.runner.runAction(data);
 
298
  }
299
  } else {
300
- artifact.runner.runAction(data);
301
  }
302
  }
303
 
@@ -312,8 +328,7 @@ export class WorkbenchStore {
312
 
313
  for (const [filePath, dirent] of Object.entries(files)) {
314
  if (dirent?.type === 'file' && !dirent.isBinary) {
315
- // remove '/home/project/' from the beginning of the path
316
- const relativePath = filePath.replace(/^\/home\/project\//, '');
317
 
318
  // split the path into segments
319
  const pathSegments = relativePath.split('/');
@@ -343,7 +358,7 @@ export class WorkbenchStore {
343
 
344
  for (const [filePath, dirent] of Object.entries(files)) {
345
  if (dirent?.type === 'file' && !dirent.isBinary) {
346
- const relativePath = filePath.replace(/^\/home\/project\//, '');
347
  const pathSegments = relativePath.split('/');
348
  let currentHandle = targetHandle;
349
 
@@ -417,7 +432,7 @@ export class WorkbenchStore {
417
  content: Buffer.from(dirent.content).toString('base64'),
418
  encoding: 'base64',
419
  });
420
- return { path: filePath.replace(/^\/home\/project\//, ''), sha: blob.sha };
421
  }
422
  })
423
  );
 
14
  import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
15
  import * as nodePath from 'node:path';
16
  import type { WebContainerProcess } from '@webcontainer/api';
17
+ import { extractRelativePath } from '~/utils/diff';
18
 
19
  export interface ArtifactState {
20
  id: string;
 
43
  modifiedFiles = new Set<string>();
44
  artifactIdList: string[] = [];
45
  #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined;
46
+ #globalExecutionQueue=Promise.resolve();
47
  constructor() {
48
  if (import.meta.hot) {
49
  import.meta.hot.data.artifacts = this.artifacts;
 
53
  }
54
  }
55
 
56
+ addToExecutionQueue(callback: () => Promise<void>) {
57
+ this.#globalExecutionQueue=this.#globalExecutionQueue.then(()=>callback())
58
+ }
59
+
60
  get previews() {
61
  return this.#previewsStore.previews;
62
  }
 
260
 
261
  this.artifacts.setKey(messageId, { ...artifact, ...state });
262
  }
263
+ addAction(data: ActionCallbackData) {
264
+ this._addAction(data)
265
+ // this.addToExecutionQueue(()=>this._addAction(data))
266
+ }
267
+ async _addAction(data: ActionCallbackData) {
268
  const { messageId } = data;
269
 
270
  const artifact = this.#getArtifact(messageId);
 
273
  unreachable('Artifact not found');
274
  }
275
 
276
+ return artifact.runner.addAction(data);
277
  }
278
 
279
+ runAction(data: ActionCallbackData, isStreaming: boolean = false) {
280
+ if(isStreaming) {
281
+ this._runAction(data, isStreaming)
282
+ }
283
+ else{
284
+ this.addToExecutionQueue(()=>this._runAction(data, isStreaming))
285
+ }
286
+ }
287
+ async _runAction(data: ActionCallbackData, isStreaming: boolean = false) {
288
  const { messageId } = data;
289
 
290
  const artifact = this.#getArtifact(messageId);
 
309
  this.#editorStore.updateFile(fullPath, data.action.content);
310
 
311
  if (!isStreaming) {
 
312
  await artifact.runner.runAction(data);
313
+ this.resetAllFileModifications();
314
  }
315
  } else {
316
+ await artifact.runner.runAction(data);
317
  }
318
  }
319
 
 
328
 
329
  for (const [filePath, dirent] of Object.entries(files)) {
330
  if (dirent?.type === 'file' && !dirent.isBinary) {
331
+ const relativePath = extractRelativePath(filePath);
 
332
 
333
  // split the path into segments
334
  const pathSegments = relativePath.split('/');
 
358
 
359
  for (const [filePath, dirent] of Object.entries(files)) {
360
  if (dirent?.type === 'file' && !dirent.isBinary) {
361
+ const relativePath = extractRelativePath(filePath);
362
  const pathSegments = relativePath.split('/');
363
  let currentHandle = targetHandle;
364
 
 
432
  content: Buffer.from(dirent.content).toString('base64'),
433
  encoding: 'base64',
434
  });
435
+ return { path: extractRelativePath(filePath), sha: blob.sha };
436
  }
437
  })
438
  );
app/utils/constants.ts CHANGED
@@ -71,7 +71,19 @@ const PROVIDER_LIST: ProviderInfo[] = [
71
  { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq' }
72
  ],
73
  getApiKeyLink: 'https://console.groq.com/keys'
74
- }, {
 
 
 
 
 
 
 
 
 
 
 
 
75
  name: 'OpenAI',
76
  staticModels: [
77
  { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' },
 
71
  { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq' }
72
  ],
73
  getApiKeyLink: 'https://console.groq.com/keys'
74
+ },
75
+ {
76
+ name: 'HuggingFace',
77
+ staticModels: [
78
+ { name: 'Qwen/Qwen2.5-Coder-32B-Instruct', label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)', provider: 'HuggingFace' },
79
+ { name: '01-ai/Yi-1.5-34B-Chat', label: 'Yi-1.5-34B-Chat (HuggingFace)', provider: 'HuggingFace' },
80
+ { name: 'codellama/CodeLlama-34b-Instruct-hf', label: 'CodeLlama-34b-Instruct (HuggingFace)', provider: 'HuggingFace' },
81
+ { name: 'NousResearch/Hermes-3-Llama-3.1-8B', label: 'Hermes-3-Llama-3.1-8B (HuggingFace)', provider: 'HuggingFace' }
82
+ ],
83
+ getApiKeyLink: 'https://huggingface.co/settings/tokens'
84
+ },
85
+
86
+ {
87
  name: 'OpenAI',
88
  staticModels: [
89
  { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' },
app/utils/diff.spec.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import { extractRelativePath } from './diff';
3
+ import { WORK_DIR } from './constants';
4
+
5
+ describe('Diff', () => {
6
+ it('should strip out Work_dir', () => {
7
+ const filePath = `${WORK_DIR}/index.js`;
8
+ const result = extractRelativePath(filePath);
9
+ expect(result).toBe('index.js');
10
+ });
11
+ });
app/utils/diff.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { createTwoFilesPatch } from 'diff';
2
  import type { FileMap } from '~/lib/stores/files';
3
- import { MODIFICATIONS_TAG_NAME } from './constants';
4
 
5
  export const modificationsRegex = new RegExp(
6
  `^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
@@ -75,6 +75,15 @@ export function diffFiles(fileName: string, oldFileContent: string, newFileConte
75
  return unifiedDiff;
76
  }
77
 
 
 
 
 
 
 
 
 
 
78
  /**
79
  * Converts the unified diff to HTML.
80
  *
 
1
  import { createTwoFilesPatch } from 'diff';
2
  import type { FileMap } from '~/lib/stores/files';
3
+ import { MODIFICATIONS_TAG_NAME, WORK_DIR } from './constants';
4
 
5
  export const modificationsRegex = new RegExp(
6
  `^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
 
75
  return unifiedDiff;
76
  }
77
 
78
+ const regex = new RegExp(`^${WORK_DIR}\/`);
79
+
80
+ /**
81
+ * Strips out the work directory from the file path.
82
+ */
83
+ export function extractRelativePath(filePath: string) {
84
+ return filePath.replace(regex, '');
85
+ }
86
+
87
  /**
88
  * Converts the unified diff to HTML.
89
  *
docker-compose.yaml CHANGED
@@ -14,6 +14,7 @@ services:
14
  # No strictly neded but serving as hints for Coolify
15
  - PORT=5173
16
  - GROQ_API_KEY=${GROQ_API_KEY}
 
17
  - OPENAI_API_KEY=${OPENAI_API_KEY}
18
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
19
  - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
@@ -40,6 +41,7 @@ services:
40
  - WATCHPACK_POLLING=true
41
  - PORT=5173
42
  - GROQ_API_KEY=${GROQ_API_KEY}
 
43
  - OPENAI_API_KEY=${OPENAI_API_KEY}
44
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
45
  - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
 
14
  # No strictly neded but serving as hints for Coolify
15
  - PORT=5173
16
  - GROQ_API_KEY=${GROQ_API_KEY}
17
+ - HuggingFace_API_KEY=${HuggingFace_API_KEY}
18
  - OPENAI_API_KEY=${OPENAI_API_KEY}
19
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
20
  - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
 
41
  - WATCHPACK_POLLING=true
42
  - PORT=5173
43
  - GROQ_API_KEY=${GROQ_API_KEY}
44
+ - HuggingFace_API_KEY=${HuggingFace_API_KEY}
45
  - OPENAI_API_KEY=${OPENAI_API_KEY}
46
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
47
  - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
eslint.config.mjs CHANGED
@@ -4,7 +4,7 @@ import { getNamingConventionRule, tsFileExtensions } from '@blitz/eslint-plugin/
4
 
5
  export default [
6
  {
7
- ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build'],
8
  },
9
  ...blitzPlugin.configs.recommended(),
10
  {
 
4
 
5
  export default [
6
  {
7
+ ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build', '**/.history'],
8
  },
9
  ...blitzPlugin.configs.recommended(),
10
  {
package.json CHANGED
@@ -54,6 +54,7 @@
54
  "@openrouter/ai-sdk-provider": "^0.0.5",
55
  "@radix-ui/react-dialog": "^1.1.1",
56
  "@radix-ui/react-dropdown-menu": "^2.1.1",
 
57
  "@remix-run/cloudflare": "^2.10.2",
58
  "@remix-run/cloudflare-pages": "^2.10.2",
59
  "@remix-run/react": "^2.10.2",
 
54
  "@openrouter/ai-sdk-provider": "^0.0.5",
55
  "@radix-ui/react-dialog": "^1.1.1",
56
  "@radix-ui/react-dropdown-menu": "^2.1.1",
57
+ "@radix-ui/react-tooltip": "^1.1.4",
58
  "@remix-run/cloudflare": "^2.10.2",
59
  "@remix-run/cloudflare-pages": "^2.10.2",
60
  "@remix-run/react": "^2.10.2",
pnpm-lock.yaml CHANGED
@@ -95,6 +95,9 @@ importers:
95
  '@radix-ui/react-dropdown-menu':
96
  specifier: ^2.1.1
97
 
 
 
98
  '@remix-run/cloudflare':
99
  specifier: ^2.10.2
100
  version: 2.10.2(@cloudflare/[email protected])([email protected])
@@ -1380,6 +1383,15 @@ packages:
1380
  '@types/react':
1381
  optional: true
1382
 
 
 
 
 
 
 
 
 
 
1383
  '@radix-ui/[email protected]':
1384
  resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
1385
  peerDependencies:
@@ -1415,6 +1427,19 @@ packages:
1415
  '@types/react-dom':
1416
  optional: true
1417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1418
  '@radix-ui/[email protected]':
1419
  resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==}
1420
  peerDependencies:
@@ -1498,6 +1523,19 @@ packages:
1498
  '@types/react-dom':
1499
  optional: true
1500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1501
  '@radix-ui/[email protected]':
1502
  resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
1503
  peerDependencies:
@@ -1511,6 +1549,19 @@ packages:
1511
  '@types/react-dom':
1512
  optional: true
1513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1514
  '@radix-ui/[email protected]':
1515
  resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
1516
  peerDependencies:
@@ -1546,6 +1597,19 @@ packages:
1546
  '@types/react':
1547
  optional: true
1548
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1549
  '@radix-ui/[email protected]':
1550
  resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
1551
  peerDependencies:
@@ -1600,6 +1664,19 @@ packages:
1600
  '@types/react':
1601
  optional: true
1602
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
  '@radix-ui/[email protected]':
1604
  resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
1605
 
@@ -6720,6 +6797,12 @@ snapshots:
6720
  optionalDependencies:
6721
  '@types/react': 18.3.3
6722
 
 
 
 
 
 
 
6723
6724
  dependencies:
6725
  '@radix-ui/primitive': 1.1.0
@@ -6761,6 +6844,19 @@ snapshots:
6761
  '@types/react': 18.3.3
6762
  '@types/react-dom': 18.3.0
6763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6764
6765
  dependencies:
6766
  '@radix-ui/primitive': 1.1.0
@@ -6854,6 +6950,16 @@ snapshots:
6854
  '@types/react': 18.3.3
6855
  '@types/react-dom': 18.3.0
6856
 
 
 
 
 
 
 
 
 
 
 
6857
6858
  dependencies:
6859
  '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
@@ -6864,6 +6970,16 @@ snapshots:
6864
  '@types/react': 18.3.3
6865
  '@types/react-dom': 18.3.0
6866
 
 
 
 
 
 
 
 
 
 
 
6867
6868
  dependencies:
6869
  '@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
@@ -6897,6 +7013,26 @@ snapshots:
6897
  optionalDependencies:
6898
  '@types/react': 18.3.3
6899
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6900
6901
  dependencies:
6902
  react: 18.3.1
@@ -6937,6 +7073,15 @@ snapshots:
6937
  optionalDependencies:
6938
  '@types/react': 18.3.3
6939
 
 
 
 
 
 
 
 
 
 
6940
  '@radix-ui/[email protected]': {}
6941
 
6942
 
95
  '@radix-ui/react-dropdown-menu':
96
  specifier: ^2.1.1
97
98
+ '@radix-ui/react-tooltip':
99
+ specifier: ^1.1.4
100
101
  '@remix-run/cloudflare':
102
  specifier: ^2.10.2
103
  version: 2.10.2(@cloudflare/[email protected])([email protected])
 
1383
  '@types/react':
1384
  optional: true
1385
 
1386
+ '@radix-ui/[email protected]':
1387
+ resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
1388
+ peerDependencies:
1389
+ '@types/react': '*'
1390
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1391
+ peerDependenciesMeta:
1392
+ '@types/react':
1393
+ optional: true
1394
+
1395
  '@radix-ui/[email protected]':
1396
  resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
1397
  peerDependencies:
 
1427
  '@types/react-dom':
1428
  optional: true
1429
 
1430
+ '@radix-ui/[email protected]':
1431
+ resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
1432
+ peerDependencies:
1433
+ '@types/react': '*'
1434
+ '@types/react-dom': '*'
1435
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1436
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1437
+ peerDependenciesMeta:
1438
+ '@types/react':
1439
+ optional: true
1440
+ '@types/react-dom':
1441
+ optional: true
1442
+
1443
  '@radix-ui/[email protected]':
1444
  resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==}
1445
  peerDependencies:
 
1523
  '@types/react-dom':
1524
  optional: true
1525
 
1526
+ '@radix-ui/[email protected]':
1527
+ resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
1528
+ peerDependencies:
1529
+ '@types/react': '*'
1530
+ '@types/react-dom': '*'
1531
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1532
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1533
+ peerDependenciesMeta:
1534
+ '@types/react':
1535
+ optional: true
1536
+ '@types/react-dom':
1537
+ optional: true
1538
+
1539
  '@radix-ui/[email protected]':
1540
  resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
1541
  peerDependencies:
 
1549
  '@types/react-dom':
1550
  optional: true
1551
 
1552
+ '@radix-ui/[email protected]':
1553
+ resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==}
1554
+ peerDependencies:
1555
+ '@types/react': '*'
1556
+ '@types/react-dom': '*'
1557
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1558
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1559
+ peerDependenciesMeta:
1560
+ '@types/react':
1561
+ optional: true
1562
+ '@types/react-dom':
1563
+ optional: true
1564
+
1565
  '@radix-ui/[email protected]':
1566
  resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
1567
  peerDependencies:
 
1597
  '@types/react':
1598
  optional: true
1599
 
1600
+ '@radix-ui/[email protected]':
1601
+ resolution: {integrity: sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==}
1602
+ peerDependencies:
1603
+ '@types/react': '*'
1604
+ '@types/react-dom': '*'
1605
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1606
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1607
+ peerDependenciesMeta:
1608
+ '@types/react':
1609
+ optional: true
1610
+ '@types/react-dom':
1611
+ optional: true
1612
+
1613
  '@radix-ui/[email protected]':
1614
  resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
1615
  peerDependencies:
 
1664
  '@types/react':
1665
  optional: true
1666
 
1667
+ '@radix-ui/[email protected]':
1668
+ resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
1669
+ peerDependencies:
1670
+ '@types/react': '*'
1671
+ '@types/react-dom': '*'
1672
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1673
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1674
+ peerDependenciesMeta:
1675
+ '@types/react':
1676
+ optional: true
1677
+ '@types/react-dom':
1678
+ optional: true
1679
+
1680
  '@radix-ui/[email protected]':
1681
  resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
1682
 
 
6797
  optionalDependencies:
6798
  '@types/react': 18.3.3
6799
 
6800
6801
+ dependencies:
6802
+ react: 18.3.1
6803
+ optionalDependencies:
6804
+ '@types/react': 18.3.3
6805
+
6806
6807
  dependencies:
6808
  '@radix-ui/primitive': 1.1.0
 
6844
  '@types/react': 18.3.3
6845
  '@types/react-dom': 18.3.0
6846
 
6847
6848
+ dependencies:
6849
+ '@radix-ui/primitive': 1.1.0
6850
+ '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
6851
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
6852
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
6853
+ '@radix-ui/react-use-escape-keydown': 1.1.0(@types/[email protected])([email protected])
6854
+ react: 18.3.1
6855
+ react-dom: 18.3.1([email protected])
6856
+ optionalDependencies:
6857
+ '@types/react': 18.3.3
6858
+ '@types/react-dom': 18.3.0
6859
+
6860
6861
  dependencies:
6862
  '@radix-ui/primitive': 1.1.0
 
6950
  '@types/react': 18.3.3
6951
  '@types/react-dom': 18.3.0
6952
 
6953
6954
+ dependencies:
6955
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
6956
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
6957
+ react: 18.3.1
6958
+ react-dom: 18.3.1([email protected])
6959
+ optionalDependencies:
6960
+ '@types/react': 18.3.3
6961
+ '@types/react-dom': 18.3.0
6962
+
6963
6964
  dependencies:
6965
  '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
 
6970
  '@types/react': 18.3.3
6971
  '@types/react-dom': 18.3.0
6972
 
6973
6974
+ dependencies:
6975
+ '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
6976
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
6977
+ react: 18.3.1
6978
+ react-dom: 18.3.1([email protected])
6979
+ optionalDependencies:
6980
+ '@types/react': 18.3.3
6981
+ '@types/react-dom': 18.3.0
6982
+
6983
6984
  dependencies:
6985
  '@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
 
7013
  optionalDependencies:
7014
  '@types/react': 18.3.3
7015
 
7016
7017
+ dependencies:
7018
+ '@radix-ui/primitive': 1.1.0
7019
+ '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
7020
+ '@radix-ui/react-context': 1.1.1(@types/[email protected])([email protected])
7021
+ '@radix-ui/react-dismissable-layer': 1.1.1(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7022
+ '@radix-ui/react-id': 1.1.0(@types/[email protected])([email protected])
7023
7024
7025
+ '@radix-ui/react-presence': 1.1.1(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7026
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7027
+ '@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
7028
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/[email protected])([email protected])
7029
+ '@radix-ui/react-visually-hidden': 1.1.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7030
+ react: 18.3.1
7031
+ react-dom: 18.3.1([email protected])
7032
+ optionalDependencies:
7033
+ '@types/react': 18.3.3
7034
+ '@types/react-dom': 18.3.0
7035
+
7036
7037
  dependencies:
7038
  react: 18.3.1
 
7073
  optionalDependencies:
7074
  '@types/react': 18.3.3
7075
 
7076
7077
+ dependencies:
7078
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7079
+ react: 18.3.1
7080
+ react-dom: 18.3.1([email protected])
7081
+ optionalDependencies:
7082
+ '@types/react': 18.3.3
7083
+ '@types/react-dom': 18.3.0
7084
+
7085
  '@radix-ui/[email protected]': {}
7086
 
7087
worker-configuration.d.ts CHANGED
@@ -2,6 +2,7 @@ interface Env {
2
  ANTHROPIC_API_KEY: string;
3
  OPENAI_API_KEY: string;
4
  GROQ_API_KEY: string;
 
5
  OPEN_ROUTER_API_KEY: string;
6
  OLLAMA_API_BASE_URL: string;
7
  OPENAI_LIKE_API_KEY: string;
 
2
  ANTHROPIC_API_KEY: string;
3
  OPENAI_API_KEY: string;
4
  GROQ_API_KEY: string;
5
+ HuggingFace_API_KEY: string;
6
  OPEN_ROUTER_API_KEY: string;
7
  OLLAMA_API_BASE_URL: string;
8
  OPENAI_LIKE_API_KEY: string;