veryfront 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/esm/cli/app/data/slug-words.d.ts.map +1 -1
  2. package/esm/cli/app/data/slug-words.js +225 -90
  3. package/esm/cli/app/operations/project-creation.js +4 -3
  4. package/esm/cli/app/shell.js +1 -1
  5. package/esm/cli/app/utils.d.ts +5 -4
  6. package/esm/cli/app/utils.d.ts.map +1 -1
  7. package/esm/cli/app/utils.js +0 -23
  8. package/esm/cli/app/views/dashboard.d.ts +1 -1
  9. package/esm/cli/app/views/dashboard.d.ts.map +1 -1
  10. package/esm/cli/app/views/dashboard.js +22 -4
  11. package/esm/cli/auth/callback-server.d.ts.map +1 -1
  12. package/esm/cli/auth/callback-server.js +3 -2
  13. package/esm/cli/commands/dev/handler.d.ts.map +1 -1
  14. package/esm/cli/commands/dev/handler.js +2 -0
  15. package/esm/cli/commands/init/init-command.d.ts.map +1 -1
  16. package/esm/cli/commands/init/init-command.js +20 -3
  17. package/esm/cli/commands/init/interactive-wizard.d.ts +3 -2
  18. package/esm/cli/commands/init/interactive-wizard.d.ts.map +1 -1
  19. package/esm/cli/commands/init/interactive-wizard.js +55 -27
  20. package/esm/cli/mcp/remote-file-tools.d.ts +0 -6
  21. package/esm/cli/mcp/remote-file-tools.d.ts.map +1 -1
  22. package/esm/cli/mcp/remote-file-tools.js +37 -15
  23. package/esm/cli/shared/reserve-slug.d.ts.map +1 -1
  24. package/esm/cli/shared/reserve-slug.js +8 -3
  25. package/esm/cli/utils/env-prompt.d.ts.map +1 -1
  26. package/esm/cli/utils/env-prompt.js +3 -0
  27. package/esm/deno.d.ts +2 -1
  28. package/esm/deno.js +8 -4
  29. package/esm/src/agent/chat-handler.d.ts +4 -3
  30. package/esm/src/agent/chat-handler.d.ts.map +1 -1
  31. package/esm/src/agent/chat-handler.js +55 -4
  32. package/esm/src/agent/react/index.d.ts +1 -1
  33. package/esm/src/agent/react/index.d.ts.map +1 -1
  34. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.d.ts +18 -0
  35. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.d.ts.map +1 -0
  36. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.js +54 -0
  37. package/esm/src/agent/react/use-chat/browser-inference/types.d.ts +43 -0
  38. package/esm/src/agent/react/use-chat/browser-inference/types.d.ts.map +1 -0
  39. package/esm/src/agent/react/use-chat/browser-inference/types.js +4 -0
  40. package/esm/src/agent/react/use-chat/browser-inference/worker-client.d.ts +23 -0
  41. package/esm/src/agent/react/use-chat/browser-inference/worker-client.d.ts.map +1 -0
  42. package/esm/src/agent/react/use-chat/browser-inference/worker-client.js +67 -0
  43. package/esm/src/agent/react/use-chat/browser-inference/worker-script.d.ts +8 -0
  44. package/esm/src/agent/react/use-chat/browser-inference/worker-script.d.ts.map +1 -0
  45. package/esm/src/agent/react/use-chat/browser-inference/worker-script.js +97 -0
  46. package/esm/src/agent/react/use-chat/index.d.ts +1 -1
  47. package/esm/src/agent/react/use-chat/index.d.ts.map +1 -1
  48. package/esm/src/agent/react/use-chat/types.d.ts +12 -0
  49. package/esm/src/agent/react/use-chat/types.d.ts.map +1 -1
  50. package/esm/src/agent/react/use-chat/use-chat.d.ts.map +1 -1
  51. package/esm/src/agent/react/use-chat/use-chat.js +120 -6
  52. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  53. package/esm/src/agent/runtime/index.js +59 -7
  54. package/esm/src/build/production-build/templates.d.ts +2 -2
  55. package/esm/src/build/production-build/templates.d.ts.map +1 -1
  56. package/esm/src/build/production-build/templates.js +2 -68
  57. package/esm/src/chat/index.d.ts +1 -1
  58. package/esm/src/chat/index.d.ts.map +1 -1
  59. package/esm/src/errors/veryfront-error.d.ts +3 -0
  60. package/esm/src/errors/veryfront-error.d.ts.map +1 -1
  61. package/esm/src/platform/adapters/runtime/deno/adapter.d.ts.map +1 -1
  62. package/esm/src/platform/adapters/runtime/deno/adapter.js +5 -1
  63. package/esm/src/platform/compat/http/deno-server.d.ts.map +1 -1
  64. package/esm/src/platform/compat/http/deno-server.js +3 -2
  65. package/esm/src/provider/index.d.ts +1 -1
  66. package/esm/src/provider/index.d.ts.map +1 -1
  67. package/esm/src/provider/index.js +1 -1
  68. package/esm/src/provider/local/ai-sdk-adapter.d.ts +19 -0
  69. package/esm/src/provider/local/ai-sdk-adapter.d.ts.map +1 -0
  70. package/esm/src/provider/local/ai-sdk-adapter.js +164 -0
  71. package/esm/src/provider/local/env.d.ts +10 -0
  72. package/esm/src/provider/local/env.d.ts.map +1 -0
  73. package/esm/src/provider/local/env.js +23 -0
  74. package/esm/src/provider/local/local-engine.d.ts +61 -0
  75. package/esm/src/provider/local/local-engine.d.ts.map +1 -0
  76. package/esm/src/provider/local/local-engine.js +211 -0
  77. package/esm/src/provider/local/model-catalog.d.ts +30 -0
  78. package/esm/src/provider/local/model-catalog.d.ts.map +1 -0
  79. package/esm/src/provider/local/model-catalog.js +58 -0
  80. package/esm/src/provider/model-registry.d.ts +14 -0
  81. package/esm/src/provider/model-registry.d.ts.map +1 -1
  82. package/esm/src/provider/model-registry.js +58 -2
  83. package/esm/src/proxy/main.js +34 -6
  84. package/esm/src/proxy/server-resolver.d.ts +23 -0
  85. package/esm/src/proxy/server-resolver.d.ts.map +1 -0
  86. package/esm/src/proxy/server-resolver.js +124 -0
  87. package/esm/src/react/components/ai/chat/components/inference-badge.d.ts +8 -0
  88. package/esm/src/react/components/ai/chat/components/inference-badge.d.ts.map +1 -0
  89. package/esm/src/react/components/ai/chat/components/inference-badge.js +36 -0
  90. package/esm/src/react/components/ai/chat/components/upgrade-cta.d.ts +7 -0
  91. package/esm/src/react/components/ai/chat/components/upgrade-cta.d.ts.map +1 -0
  92. package/esm/src/react/components/ai/chat/components/upgrade-cta.js +33 -0
  93. package/esm/src/react/components/ai/chat/index.d.ts +7 -1
  94. package/esm/src/react/components/ai/chat/index.d.ts.map +1 -1
  95. package/esm/src/react/components/ai/chat/index.js +16 -4
  96. package/package.json +5 -1
  97. package/src/cli/app/data/slug-words.ts +225 -90
  98. package/src/cli/app/operations/project-creation.ts +3 -3
  99. package/src/cli/app/shell.ts +1 -1
  100. package/src/cli/app/utils.ts +0 -30
  101. package/src/cli/app/views/dashboard.ts +27 -4
  102. package/src/cli/auth/callback-server.ts +3 -2
  103. package/src/cli/commands/dev/handler.ts +2 -0
  104. package/src/cli/commands/init/init-command.ts +30 -3
  105. package/src/cli/commands/init/interactive-wizard.ts +62 -34
  106. package/src/cli/mcp/remote-file-tools.ts +50 -15
  107. package/src/cli/shared/reserve-slug.ts +9 -2
  108. package/src/cli/utils/env-prompt.ts +3 -0
  109. package/src/deno.js +8 -4
  110. package/src/src/agent/chat-handler.ts +57 -4
  111. package/src/src/agent/react/index.ts +2 -0
  112. package/src/src/agent/react/use-chat/browser-inference/browser-engine.ts +81 -0
  113. package/src/src/agent/react/use-chat/browser-inference/types.ts +52 -0
  114. package/src/src/agent/react/use-chat/browser-inference/worker-client.ts +89 -0
  115. package/src/src/agent/react/use-chat/browser-inference/worker-script.ts +98 -0
  116. package/src/src/agent/react/use-chat/index.ts +2 -0
  117. package/src/src/agent/react/use-chat/types.ts +20 -0
  118. package/src/src/agent/react/use-chat/use-chat.ts +148 -8
  119. package/src/src/agent/runtime/index.ts +72 -6
  120. package/src/src/build/production-build/templates.ts +2 -68
  121. package/src/src/chat/index.ts +2 -0
  122. package/src/src/errors/veryfront-error.ts +2 -1
  123. package/src/src/platform/adapters/runtime/deno/adapter.ts +5 -1
  124. package/src/src/platform/compat/http/deno-server.ts +3 -1
  125. package/src/src/provider/index.ts +1 -0
  126. package/src/src/provider/local/ai-sdk-adapter.ts +207 -0
  127. package/src/src/provider/local/env.ts +26 -0
  128. package/src/src/provider/local/local-engine.ts +288 -0
  129. package/src/src/provider/local/model-catalog.ts +73 -0
  130. package/src/src/provider/model-registry.ts +66 -2
  131. package/src/src/proxy/main.ts +41 -6
  132. package/src/src/proxy/server-resolver.ts +151 -0
  133. package/src/src/react/components/ai/chat/components/inference-badge.tsx +48 -0
  134. package/src/src/react/components/ai/chat/components/upgrade-cta.tsx +56 -0
  135. package/src/src/react/components/ai/chat/index.tsx +43 -6
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Dedicated Server Resolver
3
+ *
4
+ * Resolves an environment ID to a dedicated server hostname.
5
+ * Used by the proxy to route traffic to dedicated servers instead of the shared pool.
6
+ *
7
+ * Caches results in memory with a short TTL to avoid hitting the API on every request.
8
+ * A null result (no dedicated server) is also cached to prevent repeated lookups.
9
+ */
10
+ import * as dntShim from "../../_dnt.shims.js";
11
+
12
+
13
+ import { proxyLogger } from "./logger.js";
14
+ import { unrefTimer } from "../platform/compat/process.js";
15
+
16
+ const logger = proxyLogger.child({ module: "server-resolver" });
17
+
18
+ interface DedicatedServer {
19
+ id: string;
20
+ short_id: string;
21
+ hostname: string;
22
+ status: string;
23
+ }
24
+
25
+ interface CacheEntry {
26
+ server: DedicatedServer | null;
27
+ expiresAt: number;
28
+ }
29
+
30
+ /** Thrown when the API call fails (network error, non-OK status, parse error). */
31
+ class ServerResolverError extends Error {
32
+ constructor(message: string, options?: { cause?: unknown }) {
33
+ super(message, options);
34
+ }
35
+ }
36
+
37
+ export class ServerResolver {
38
+ private cache = new Map<string, CacheEntry>();
39
+ private pending = new Map<string, Promise<DedicatedServer | null>>();
40
+ private cleanupTimer: ReturnType<typeof dntShim.setInterval> | null = null;
41
+
42
+ constructor(
43
+ private apiInternalUrl: string,
44
+ private apiUser: string,
45
+ private apiPass: string,
46
+ private cacheTtlMs: number = 30_000,
47
+ ) {
48
+ // Cleanup expired entries every 60s
49
+ this.cleanupTimer = dntShim.setInterval(() => this.cleanup(), 60_000);
50
+ // Don't keep the process alive for cleanup
51
+ unrefTimer(this.cleanupTimer);
52
+ }
53
+
54
+ /**
55
+ * Resolve an environment ID to a dedicated server URL, or null for shared pool.
56
+ */
57
+ async resolve(environmentId: string | undefined): Promise<string | null> {
58
+ if (!environmentId) return null;
59
+
60
+ const cached = this.cache.get(environmentId);
61
+ if (cached && Date.now() < cached.expiresAt) {
62
+ return cached.server ? `http://${cached.server.hostname}` : null;
63
+ }
64
+
65
+ // Deduplicate concurrent requests for the same environment
66
+ const inflight = this.pending.get(environmentId);
67
+ if (inflight) {
68
+ const server = await inflight;
69
+ return server ? `http://${server.hostname}` : null;
70
+ }
71
+
72
+ const promise = this.fetchServer(environmentId);
73
+ this.pending.set(environmentId, promise);
74
+
75
+ try {
76
+ const server = await promise;
77
+ // Only cache successful API responses (server found OR explicit "no server").
78
+ // Transient errors (network failures, non-OK status) are NOT cached so the
79
+ // next request retries the API instead of suppressing dedicated routing.
80
+ this.cache.set(environmentId, {
81
+ server,
82
+ expiresAt: Date.now() + this.cacheTtlMs,
83
+ });
84
+ return server ? `http://${server.hostname}` : null;
85
+ } catch (error) {
86
+ // API error — don't cache, fall back to shared pool for this request
87
+ logger.warn("[ServerResolver] Transient error, skipping cache", {
88
+ environmentId,
89
+ error: error instanceof Error ? error.message : String(error),
90
+ });
91
+ return null;
92
+ } finally {
93
+ this.pending.delete(environmentId);
94
+ }
95
+ }
96
+
97
+ close(): void {
98
+ if (this.cleanupTimer) {
99
+ clearInterval(this.cleanupTimer);
100
+ this.cleanupTimer = null;
101
+ }
102
+ this.cache.clear();
103
+ }
104
+
105
+ /**
106
+ * Fetch dedicated server from API.
107
+ * Returns DedicatedServer | null on success (null = no dedicated server assigned).
108
+ * Throws ServerResolverError on transient failures (network, non-OK status).
109
+ */
110
+ private async fetchServer(environmentId: string): Promise<DedicatedServer | null> {
111
+ const url = `${this.apiInternalUrl}/internal/environment-server?environmentId=${
112
+ encodeURIComponent(environmentId)
113
+ }`;
114
+ const headers: Record<string, string> = { Accept: "application/json" };
115
+
116
+ if (this.apiUser && this.apiPass) {
117
+ headers.Authorization = `Basic ${btoa(`${this.apiUser}:${this.apiPass}`)}`;
118
+ }
119
+
120
+ let response: dntShim.Response;
121
+ try {
122
+ response = await dntShim.fetch(url, { headers, signal: AbortSignal.timeout(5_000) });
123
+ } catch (error) {
124
+ throw new ServerResolverError(
125
+ `Failed to reach API: ${error instanceof Error ? error.message : String(error)}`,
126
+ { cause: error },
127
+ );
128
+ }
129
+
130
+ if (!response.ok) {
131
+ await response.body?.cancel();
132
+ throw new ServerResolverError(`API returned ${response.status} for ${environmentId}`);
133
+ }
134
+
135
+ const data = (await response.json()) as { server: DedicatedServer | null };
136
+ if (data.server) {
137
+ logger.debug("[ServerResolver] Resolved dedicated server", {
138
+ environmentId,
139
+ hostname: data.server.hostname,
140
+ });
141
+ }
142
+ return data.server;
143
+ }
144
+
145
+ private cleanup(): void {
146
+ const now = Date.now();
147
+ for (const [key, entry] of this.cache) {
148
+ if (now >= entry.expiresAt) this.cache.delete(key);
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+ import type { BrowserInferenceStatus, InferenceMode } from "../../../../../agent/react/index.js";
3
+
4
+ export interface InferenceBadgeProps {
5
+ inferenceMode: InferenceMode;
6
+ browserStatus?: BrowserInferenceStatus | null;
7
+ }
8
+
9
+ export function InferenceBadge({
10
+ inferenceMode,
11
+ browserStatus,
12
+ }: InferenceBadgeProps): React.ReactElement | null {
13
+ if (inferenceMode === "cloud") return null;
14
+
15
+ let label: string;
16
+ let dotColor: string;
17
+ let showProgress = false;
18
+
19
+ if (inferenceMode === "server-local") {
20
+ label = "Running locally";
21
+ dotColor = "bg-green-500";
22
+ } else if (browserStatus === "downloading-model") {
23
+ label = "Downloading model...";
24
+ dotColor = "bg-amber-500";
25
+ showProgress = true;
26
+ } else if (browserStatus === "loading-runtime") {
27
+ label = "Loading AI runtime...";
28
+ dotColor = "bg-amber-500";
29
+ } else if (browserStatus === "generating") {
30
+ label = "Running in browser";
31
+ dotColor = "bg-green-500";
32
+ } else if (browserStatus === "error") {
33
+ label = "Local model failed";
34
+ dotColor = "bg-red-500";
35
+ } else {
36
+ label = "Running locally";
37
+ dotColor = "bg-green-500";
38
+ }
39
+
40
+ return (
41
+ <div className="flex items-center gap-1.5 px-3 py-1 text-xs text-neutral-500 dark:text-neutral-400">
42
+ <span
43
+ className={`size-1.5 rounded-full ${dotColor} ${showProgress ? "animate-pulse" : ""}`}
44
+ />
45
+ <span>{label}</span>
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import type { InferenceMode } from "../../../../../agent/react/index.js";
3
+
4
+ const DISMISS_KEY = "vf-upgrade-cta-dismissed";
5
+
6
+ export interface UpgradeCTAProps {
7
+ inferenceMode: InferenceMode;
8
+ }
9
+
10
+ export function UpgradeCTA({ inferenceMode }: UpgradeCTAProps): React.ReactElement | null {
11
+ const [dismissed, setDismissed] = React.useState(() => {
12
+ try {
13
+ return localStorage.getItem(DISMISS_KEY) === "1";
14
+ } catch {
15
+ return false;
16
+ }
17
+ });
18
+
19
+ if (inferenceMode === "cloud" || dismissed) return null;
20
+
21
+ const handleDismiss = () => {
22
+ setDismissed(true);
23
+ try {
24
+ localStorage.setItem(DISMISS_KEY, "1");
25
+ } catch {
26
+ // localStorage may be unavailable
27
+ }
28
+ };
29
+
30
+ return (
31
+ <div className="w-full max-w-2xl mx-auto mt-4 px-4 py-3 bg-blue-50 dark:bg-blue-900/20 rounded-xl text-sm text-blue-700 dark:text-blue-300 flex items-start gap-3">
32
+ <span className="flex-1">
33
+ Using a lightweight local model. Add an API key to your{" "}
34
+ <code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-900/40 rounded text-xs">.env</code>
35
+ {" "}
36
+ for GPT-4o or Claude.
37
+ </span>
38
+ <button
39
+ type="button"
40
+ onClick={handleDismiss}
41
+ className="text-blue-400 hover:text-blue-600 dark:hover:text-blue-200 transition-colors flex-shrink-0"
42
+ aria-label="Dismiss"
43
+ >
44
+ <svg
45
+ className="size-4"
46
+ viewBox="0 0 16 16"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ strokeWidth="2"
50
+ >
51
+ <path d="M4 4l8 8M12 4l-8 8" />
52
+ </svg>
53
+ </button>
54
+ </div>
55
+ );
56
+ }
@@ -7,7 +7,13 @@ import {
7
7
  SubmitButton,
8
8
  } from "../../../primitives/index.js";
9
9
  import { useVoiceInput } from "../../../../agent/react/index.js";
10
- import type { DynamicToolUIPart, ToolUIPart, UIMessage } from "../../../../agent/react/index.js";
10
+ import type {
11
+ BrowserInferenceStatus,
12
+ DynamicToolUIPart,
13
+ InferenceMode,
14
+ ToolUIPart,
15
+ UIMessage,
16
+ } from "../../../../agent/react/index.js";
11
17
  import { type ChatTheme, cn, defaultChatTheme, mergeThemes } from "../theme.js";
12
18
  import { Markdown } from "../markdown.js";
13
19
  import { MessageSquareIcon, RefreshCwIcon } from "../icons/index.js";
@@ -27,6 +33,8 @@ export {
27
33
  } from "./components/empty-state.js";
28
34
  export { MessageActions, type MessageActionsProps } from "./components/message-actions.js";
29
35
  export { ToolCallCard, ToolStatusBadge } from "./components/tool-ui.js";
36
+ export { InferenceBadge, type InferenceBadgeProps } from "./components/inference-badge.js";
37
+ export { UpgradeCTA, type UpgradeCTAProps } from "./components/upgrade-cta.js";
30
38
 
31
39
  export {
32
40
  getTextContent,
@@ -48,6 +56,8 @@ import {
48
56
  import { MessageActions } from "./components/message-actions.js";
49
57
  import { ReasoningCard } from "./components/reasoning.js";
50
58
  import { ToolCallCard } from "./components/tool-ui.js";
59
+ import { InferenceBadge } from "./components/inference-badge.js";
60
+ import { UpgradeCTA } from "./components/upgrade-cta.js";
51
61
  import { getTextContent, groupPartsInOrder } from "./utils/message-parts.js";
52
62
 
53
63
  export interface ChatProps {
@@ -86,6 +96,10 @@ export interface ChatProps {
86
96
  model?: string;
87
97
  /** Called when user changes model */
88
98
  onModelChange?: (model: string) => void;
99
+ /** Where inference is currently happening */
100
+ inferenceMode?: InferenceMode;
101
+ /** Browser-side model loading/inference status */
102
+ browserStatus?: BrowserInferenceStatus | null;
89
103
  }
90
104
 
91
105
  export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
@@ -118,6 +132,8 @@ export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
118
132
  models,
119
133
  model,
120
134
  onModelChange,
135
+ inferenceMode,
136
+ browserStatus,
121
137
  },
122
138
  ref,
123
139
  ): React.ReactElement {
@@ -172,6 +188,9 @@ export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
172
188
  </Suggestions>
173
189
  </div>
174
190
  )}
191
+ {inferenceMode && inferenceMode !== "cloud" && (
192
+ <UpgradeCTA inferenceMode={inferenceMode} />
193
+ )}
175
194
  <div className="flex-1" />
176
195
  </div>
177
196
  )
@@ -239,11 +258,24 @@ export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
239
258
  {isLoading && (
240
259
  <div className="flex justify-start">
241
260
  <div className="bg-neutral-100 dark:bg-neutral-800 rounded-[20px] rounded-bl-[4px] px-4 py-3">
242
- <div className="flex gap-1.5 items-center">
243
- <span className={cn(theme.loading)} />
244
- <span className={cn(theme.loading)} style={{ animationDelay: "0.15s" }} />
245
- <span className={cn(theme.loading)} style={{ animationDelay: "0.3s" }} />
246
- </div>
261
+ {browserStatus === "downloading-model" || browserStatus === "loading-runtime"
262
+ ? (
263
+ <div className="flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400">
264
+ <span className="size-1.5 rounded-full bg-amber-500 animate-pulse" />
265
+ <span>
266
+ {browserStatus === "downloading-model"
267
+ ? "Downloading model..."
268
+ : "Loading AI..."}
269
+ </span>
270
+ </div>
271
+ )
272
+ : (
273
+ <div className="flex gap-1.5 items-center">
274
+ <span className={cn(theme.loading)} />
275
+ <span className={cn(theme.loading)} style={{ animationDelay: "0.15s" }} />
276
+ <span className={cn(theme.loading)} style={{ animationDelay: "0.3s" }} />
277
+ </div>
278
+ )}
247
279
  </div>
248
280
  </div>
249
281
  )}
@@ -278,6 +310,11 @@ export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
278
310
  )}
279
311
 
280
312
  <div className="flex-shrink-0 bg-white dark:bg-neutral-900 border-t border-neutral-200 dark:border-neutral-800">
313
+ {inferenceMode && inferenceMode !== "cloud" && (
314
+ <div className="max-w-2xl mx-auto">
315
+ <InferenceBadge inferenceMode={inferenceMode} browserStatus={browserStatus} />
316
+ </div>
317
+ )}
281
318
  <form onSubmit={submitHandler} className="max-w-2xl mx-auto px-4 py-3">
282
319
  {models && models.length > 0 && onModelChange && (
283
320
  <div className="mb-2">