`
)
)
);
@@ -974,14 +992,14 @@ const route3 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
loader
}, Symbol.toStringTag, { value: 'Module' }));
-const serverManifest = {'entry':{'module':'/assets/entry.client-B6sKpTHs.js','imports':['/assets/components-BGHgjcK3.js'],'css':['/assets/__uno-CiRtdAq4.css']},'routes':{'root':{'id':'root','parentId':undefined,'path':'','index':undefined,'caseSensitive':undefined,'hasAction':false,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/root-Bc5vqUK2.js','imports':['/assets/components-BGHgjcK3.js'],'css':['/assets/__uno-CiRtdAq4.css']},'routes/api.enhancer':{'id':'routes/api.enhancer','parentId':'root','path':'api/enhancer','index':undefined,'caseSensitive':undefined,'hasAction':true,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/api.enhancer-l0sNRNKZ.js','imports':[],'css':[]},'routes/app.chat':{'id':'routes/app.chat','parentId':'root','path':'app/chat','index':undefined,'caseSensitive':undefined,'hasAction':true,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/app.chat-l0sNRNKZ.js','imports':[],'css':[]},'routes/chat.$id':{'id':'routes/chat.$id','parentId':'root','path':'chat/:id','index':undefined,'caseSensitive':undefined,'hasAction':false,'hasLoader':true,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/chat._id-Gj-AyoRW.js','imports':['/assets/components-BGHgjcK3.js','/assets/_index-094Tk7yO.js'],'css':['/assets/_index-C3nBTdmK.css']},'routes/_index':{'id':'routes/_index','parentId':'root','path':undefined,'index':true,'caseSensitive':undefined,'hasAction':false,'hasLoader':true,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/_index-B3Xgmij8.js','imports':['/assets/components-BGHgjcK3.js','/assets/_index-094Tk7yO.js'],'css':['/assets/_index-C3nBTdmK.css']}},'url':'/assets/manifest-a2fe7e12.js','version':'a2fe7e12'};
+const serverManifest = {'entry':{'module':'/assets/entry.client-CaYW3QHs.js','imports':['/assets/components-DCdkgeb6.js'],'css':['/assets/__uno-D5utbcWA.css']},'routes':{'root':{'id':'root','parentId':undefined,'path':'','index':undefined,'caseSensitive':undefined,'hasAction':false,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/root-B9HAWh13.js','imports':['/assets/components-DCdkgeb6.js','/assets/theme-BD2Iu4z3.js'],'css':['/assets/__uno-D5utbcWA.css']},'routes/api.enhancer':{'id':'routes/api.enhancer','parentId':'root','path':'api/enhancer','index':undefined,'caseSensitive':undefined,'hasAction':true,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/api.enhancer-l0sNRNKZ.js','imports':[],'css':[]},'routes/app.chat':{'id':'routes/app.chat','parentId':'root','path':'app/chat','index':undefined,'caseSensitive':undefined,'hasAction':true,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/app.chat-l0sNRNKZ.js','imports':[],'css':[]},'routes/chat.$id':{'id':'routes/chat.$id','parentId':'root','path':'chat/:id','index':undefined,'caseSensitive':undefined,'hasAction':false,'hasLoader':true,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/chat._id-BbvYocFx.js','imports':['/assets/components-DCdkgeb6.js','/assets/theme-BD2Iu4z3.js','/assets/_index-DeJ4qdO3.js'],'css':['/assets/_index-C3nBTdmK.css']},'routes/_index':{'id':'routes/_index','parentId':'root','path':undefined,'index':true,'caseSensitive':undefined,'hasAction':false,'hasLoader':true,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/_index-tCCsp1PN.js','imports':['/assets/components-DCdkgeb6.js','/assets/theme-BD2Iu4z3.js','/assets/_index-DeJ4qdO3.js'],'css':['/assets/_index-C3nBTdmK.css']}},'url':'/assets/manifest-b9eadaaf.js','version':'b9eadaaf'};
/**
* `mode` is only relevant for the old Remix compiler but
* is included here to satisfy the `ServerBuild` typings.
*/
const mode = "production";
- const assetsBuildDirectory = "build\\client";
+ const assetsBuildDirectory = "build/client";
const basename = "/";
const future = {"v3_fetcherPersist":true,"v3_relativeSplatPath":true,"v3_throwAbortReason":true,"unstable_singleFetch":false,"unstable_fogOfWar":false};
const isSpaMode = false;
From a1ae83e56311bdcca09c8db664e7978afd7d5c6d Mon Sep 17 00:00:00 2001
From: Mike Robinson
Date: Mon, 29 Sep 2025 03:33:59 +1300
Subject: [PATCH 07/18] Restore Remix document layout to fix hydration
---
app/entry.server.tsx | 47 +-----------
app/root.tsx | 69 ++++++++++++++---
functions/_server/index.js | 147 +++++++++++++++++++------------------
3 files changed, 133 insertions(+), 130 deletions(-)
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index b863baf8d2..2faed2ebee 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -3,10 +3,6 @@ import { RemixServer } from '@remix-run/react';
import { isbot } from 'isbot';
import ReactDOMServer from 'react-dom/server';
import * as ReactDOMServerBrowser from 'react-dom/server.browser';
-import { renderHeadToString } from 'remix-island';
-import { Head } from './root';
-import { themeStore } from '~/lib/stores/theme';
-
export default async function handleRequest(
request: Request,
responseStatusCode: number,
@@ -34,47 +30,6 @@ export default async function handleRequest(
},
);
- const body = new ReadableStream({
- start(controller) {
- const head = renderHeadToString({ request, remixContext, Head });
-
- controller.enqueue(
- new Uint8Array(
- new TextEncoder().encode(
- `${head}`,
- ),
- ),
- );
-
- const reader = readable.getReader();
-
- function read() {
- reader
- .read()
- .then(({ done, value }) => {
- if (done) {
- controller.enqueue(new Uint8Array(new TextEncoder().encode(`
`)));
- controller.close();
-
- return;
- }
-
- controller.enqueue(value);
- read();
- })
- .catch((error) => {
- controller.error(error);
- readable.cancel();
- });
- }
- read();
- },
-
- cancel() {
- readable.cancel();
- },
- });
-
if (isbot(request.headers.get('user-agent') || '')) {
await readable.allReady;
}
@@ -84,7 +39,7 @@ export default async function handleRequest(
responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
- return new Response(body, {
+ return new Response(readable, {
headers: responseHeaders,
status: responseStatusCode,
});
diff --git a/app/root.tsx b/app/root.tsx
index 4be66b1559..634b1b86e0 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,31 +1,76 @@
// app/root.tsx
-import { Meta, Links, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
+import type { LinksFunction } from "@remix-run/cloudflare";
+import {
+ Meta,
+ Links,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+ isRouteErrorResponse,
+ useRouteError,
+} from "@remix-run/react";
+import type { ReactNode } from "react";
// Side-effect CSS imports (keep these; no ?url)
import "@unocss/reset/tailwind.css";
import "virtual:uno.css";
+import { DEFAULT_THEME, kTheme } from "~/lib/stores/theme";
+
+const themeInitScript = `(() => {
+ try {
+ const storedTheme = localStorage.getItem(${JSON.stringify(kTheme)});
+ if (storedTheme) {
+ document.documentElement.setAttribute('data-theme', storedTheme);
+ }
+ } catch {}
+})();`;
+
+export const links: LinksFunction = () => [
+ { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
+];
+
// π This is what entry.server.tsx expects to exist
-export function Head() {
- return (
- <>
-
-
- >
- );
+export default function App() {
+ return ;
}
-export default function App() {
+export function Layout({ children }: { children: ReactNode }) {
return (
-
+
-
+
+
+
-
+
+ {children}
+
);
}
+
+export function ErrorBoundary() {
+ const error = useRouteError();
+
+ let message = "Something went wrong";
+
+ if (isRouteErrorResponse(error)) {
+ message = `${error.status} ${error.statusText}`;
+ } else if (error instanceof Error) {
+ message = error.message;
+ }
+
+ return (
+
+
+
Application error
+
{message}
+
+
+ );
+}
diff --git a/functions/_server/index.js b/functions/_server/index.js
index f1c67651a9..763550d8dd 100644
--- a/functions/_server/index.js
+++ b/functions/_server/index.js
@@ -1,8 +1,8 @@
-import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
-import { Outlet, ScrollRestoration, Scripts, Meta, Links, RemixServer } from '@remix-run/react';
+import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
+import { RemixServer, Outlet, Meta, Links, ScrollRestoration, Scripts, useRouteError, isRouteErrorResponse } from '@remix-run/react';
import { isbot } from 'isbot';
-import { renderToReadableStream } from 'react-dom/server';
-import { renderHeadToString } from 'remix-island';
+import ReactDOMServer from 'react-dom/server';
+import * as ReactDOMServerBrowser from 'react-dom/server.browser';
import { atom, map } from 'nanostores';
import { json } from '@remix-run/cloudflare';
import { streamText as streamText$1, convertToCoreMessages, parseStreamPart, StreamingTextResponse } from 'ai';
@@ -13,81 +13,27 @@ import { ClientOnly } from 'remix-utils/client-only';
import React, { memo } from 'react';
import { useStore } from '@nanostores/react';
-function Head() {
- return /* @__PURE__ */ jsxs(Fragment, { children: [
- /* @__PURE__ */ jsx(Meta, {}),
- /* @__PURE__ */ jsx(Links, {})
- ] });
-}
-function App() {
- return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
- /* @__PURE__ */ jsx("head", { children: /* @__PURE__ */ jsx(Head, {}) }),
- /* @__PURE__ */ jsxs("body", { className: "min-h-dvh bg-neutral-950 text-white antialiased", children: [
- /* @__PURE__ */ jsx(Outlet, {}),
- /* @__PURE__ */ jsx(ScrollRestoration, {}),
- /* @__PURE__ */ jsx(Scripts, {})
- ] })
- ] });
-}
-
-const route0 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
- __proto__: null,
- Head,
- default: App
-}, Symbol.toStringTag, { value: 'Module' }));
-
-const DEFAULT_THEME = "light";
-const themeStore = atom(initStore());
-function initStore() {
- return DEFAULT_THEME;
-}
-
async function handleRequest(request, responseStatusCode, responseHeaders, remixContext, _loadContext) {
- const readable = await renderToReadableStream(/* @__PURE__ */ jsx(RemixServer, { context: remixContext, url: request.url }), {
- signal: request.signal,
- onError(error) {
- console.error(error);
- responseStatusCode = 500;
- }
- });
- const body = new ReadableStream({
- start(controller) {
- const head = renderHeadToString({ request, remixContext, Head });
- controller.enqueue(
- new Uint8Array(
- new TextEncoder().encode(
- `${head}
`
- )
- )
- );
- const reader = readable.getReader();
- function read() {
- reader.read().then(({ done, value }) => {
- if (done) {
- controller.enqueue(new Uint8Array(new TextEncoder().encode(`
`)));
- controller.close();
- return;
- }
- controller.enqueue(value);
- read();
- }).catch((error) => {
- controller.error(error);
- readable.cancel();
- });
+ const serverExports = ReactDOMServer;
+ const browserServerExports = ReactDOMServerBrowser;
+ const renderToReadableStream = typeof serverExports.renderToReadableStream === "function" ? serverExports.renderToReadableStream : browserServerExports.renderToReadableStream;
+ const readable = await renderToReadableStream(
+ /* @__PURE__ */ jsx(RemixServer, { context: remixContext, url: request.url }),
+ {
+ signal: request.signal,
+ onError(error) {
+ console.error(error);
+ responseStatusCode = 500;
}
- read();
- },
- cancel() {
- readable.cancel();
}
- });
+ );
if (isbot(request.headers.get("user-agent") || "")) {
await readable.allReady;
}
responseHeaders.set("Content-Type", "text/html");
responseHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
responseHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
- return new Response(body, {
+ return new Response(readable, {
headers: responseHeaders,
status: responseStatusCode
});
@@ -98,6 +44,63 @@ const entryServer = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.definePropert
default: handleRequest
}, Symbol.toStringTag, { value: 'Module' }));
+const kTheme = "bolt_theme";
+const DEFAULT_THEME = "light";
+atom(initStore());
+function initStore() {
+ return DEFAULT_THEME;
+}
+
+const themeInitScript = `(() => {
+ try {
+ const storedTheme = localStorage.getItem(${JSON.stringify(kTheme)});
+ if (storedTheme) {
+ document.documentElement.setAttribute('data-theme', storedTheme);
+ }
+ } catch {}
+})();`;
+const links = () => [
+ { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" }
+];
+function App() {
+ return /* @__PURE__ */ jsx(Outlet, {});
+}
+function Layout({ children }) {
+ return /* @__PURE__ */ jsxs("html", { lang: "en", "data-theme": DEFAULT_THEME, children: [
+ /* @__PURE__ */ jsxs("head", { children: [
+ /* @__PURE__ */ jsx(Meta, {}),
+ /* @__PURE__ */ jsx(Links, {}),
+ /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: themeInitScript } })
+ ] }),
+ /* @__PURE__ */ jsxs("body", { className: "min-h-dvh bg-neutral-950 text-white antialiased", children: [
+ /* @__PURE__ */ jsx("div", { id: "root", className: "h-full w-full", children }),
+ /* @__PURE__ */ jsx(ScrollRestoration, {}),
+ /* @__PURE__ */ jsx(Scripts, {})
+ ] })
+ ] });
+}
+function ErrorBoundary() {
+ const error = useRouteError();
+ let message = "Something went wrong";
+ if (isRouteErrorResponse(error)) {
+ message = `${error.status} ${error.statusText}`;
+ } else if (error instanceof Error) {
+ message = error.message;
+ }
+ return /* @__PURE__ */ jsx(Layout, { children: /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-2 p-6 text-center", children: [
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Application error" }),
+ /* @__PURE__ */ jsx("p", { className: "max-w-lg text-balance text-neutral-300", children: message })
+ ] }) });
+}
+
+const route0 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+ __proto__: null,
+ ErrorBoundary,
+ Layout,
+ default: App,
+ links
+}, Symbol.toStringTag, { value: 'Module' }));
+
function getAPIKey(cloudflareEnv) {
return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
}
@@ -974,14 +977,14 @@ const route3 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
loader
}, Symbol.toStringTag, { value: 'Module' }));
-const serverManifest = {'entry':{'module':'/assets/entry.client-B6sKpTHs.js','imports':['/assets/components-BGHgjcK3.js'],'css':['/assets/__uno-CiRtdAq4.css']},'routes':{'root':{'id':'root','parentId':undefined,'path':'','index':undefined,'caseSensitive':undefined,'hasAction':false,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/root-Bc5vqUK2.js','imports':['/assets/components-BGHgjcK3.js'],'css':['/assets/__uno-CiRtdAq4.css']},'routes/api.enhancer':{'id':'routes/api.enhancer','parentId':'root','path':'api/enhancer','index':undefined,'caseSensitive':undefined,'hasAction':true,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/api.enhancer-l0sNRNKZ.js','imports':[],'css':[]},'routes/app.chat':{'id':'routes/app.chat','parentId':'root','path':'app/chat','index':undefined,'caseSensitive':undefined,'hasAction':true,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/app.chat-l0sNRNKZ.js','imports':[],'css':[]},'routes/chat.$id':{'id':'routes/chat.$id','parentId':'root','path':'chat/:id','index':undefined,'caseSensitive':undefined,'hasAction':false,'hasLoader':true,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/chat._id-Gj-AyoRW.js','imports':['/assets/components-BGHgjcK3.js','/assets/_index-094Tk7yO.js'],'css':['/assets/_index-C3nBTdmK.css']},'routes/_index':{'id':'routes/_index','parentId':'root','path':undefined,'index':true,'caseSensitive':undefined,'hasAction':false,'hasLoader':true,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/_index-B3Xgmij8.js','imports':['/assets/components-BGHgjcK3.js','/assets/_index-094Tk7yO.js'],'css':['/assets/_index-C3nBTdmK.css']}},'url':'/assets/manifest-a2fe7e12.js','version':'a2fe7e12'};
+const serverManifest = {'entry':{'module':'/assets/entry.client-CjSHOzOP.js','imports':['/assets/components-DfjO86aM.js'],'css':['/assets/__uno-SwipgEyW.css']},'routes':{'root':{'id':'root','parentId':undefined,'path':'','index':undefined,'caseSensitive':undefined,'hasAction':false,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':true,'module':'/assets/root-B9nMHUOm.js','imports':['/assets/components-DfjO86aM.js','/assets/theme-B3J57z1v.js'],'css':['/assets/__uno-SwipgEyW.css']},'routes/api.enhancer':{'id':'routes/api.enhancer','parentId':'root','path':'api/enhancer','index':undefined,'caseSensitive':undefined,'hasAction':true,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/api.enhancer-l0sNRNKZ.js','imports':[],'css':[]},'routes/app.chat':{'id':'routes/app.chat','parentId':'root','path':'app/chat','index':undefined,'caseSensitive':undefined,'hasAction':true,'hasLoader':false,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/app.chat-l0sNRNKZ.js','imports':[],'css':[]},'routes/chat.$id':{'id':'routes/chat.$id','parentId':'root','path':'chat/:id','index':undefined,'caseSensitive':undefined,'hasAction':false,'hasLoader':true,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/chat._id-Nq5aHB0q.js','imports':['/assets/components-DfjO86aM.js','/assets/theme-B3J57z1v.js','/assets/_index-CJJe-0Nq.js'],'css':['/assets/_index-C3nBTdmK.css']},'routes/_index':{'id':'routes/_index','parentId':'root','path':undefined,'index':true,'caseSensitive':undefined,'hasAction':false,'hasLoader':true,'hasClientAction':false,'hasClientLoader':false,'hasErrorBoundary':false,'module':'/assets/_index-D8GfRUro.js','imports':['/assets/components-DfjO86aM.js','/assets/theme-B3J57z1v.js','/assets/_index-CJJe-0Nq.js'],'css':['/assets/_index-C3nBTdmK.css']}},'url':'/assets/manifest-beaf6609.js','version':'beaf6609'};
/**
* `mode` is only relevant for the old Remix compiler but
* is included here to satisfy the `ServerBuild` typings.
*/
const mode = "production";
- const assetsBuildDirectory = "build\\client";
+ const assetsBuildDirectory = "build/client";
const basename = "/";
const future = {"v3_fetcherPersist":true,"v3_relativeSplatPath":true,"v3_throwAbortReason":true,"unstable_singleFetch":false,"unstable_fogOfWar":false};
const isSpaMode = false;
From 30c61f5be41b54dc5350280a2086054b4f806b49 Mon Sep 17 00:00:00 2001
From: Mike Robinson
Date: Mon, 29 Sep 2025 04:10:09 +1300
Subject: [PATCH 08/18] Restore root layout and error boundary
---
app/root.tsx | 40 ++++++++++++++++++++++++++++++++++++++--
1 file changed, 38 insertions(+), 2 deletions(-)
diff --git a/app/root.tsx b/app/root.tsx
index 4fe9dddfe9..bd3770f369 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,8 +1,8 @@
// app/root.tsx
import type { LinksFunction } from "@remix-run/cloudflare";
import {
- Meta,
Links,
+ Meta,
Outlet,
Scripts,
ScrollRestoration,
@@ -36,4 +36,40 @@ export default function App() {
}
export function Layout({ children }: { children: ReactNode }) {
- return (
\ No newline at end of file
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+
+export function ErrorBoundary() {
+ const error = useRouteError();
+ let message = "Something went wrong";
+
+ if (isRouteErrorResponse(error)) {
+ message = `${error.status} ${error.statusText}`;
+ } else if (error instanceof Error) {
+ message = error.message;
+ }
+
+ return (
+
+
+
Application error
+
{message}
+
+
+ );
+}
From 3e32883e6de6a126ff223d03c5a7e5bd450c7466 Mon Sep 17 00:00:00 2001
From: Mike Robinson
Date: Mon, 29 Sep 2025 17:02:47 +1300
Subject: [PATCH 09/18] Add API chat route
---
app/routes/api.chat.ts | 1 +
1 file changed, 1 insertion(+)
create mode 100644 app/routes/api.chat.ts
diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts
new file mode 100644
index 0000000000..9f8e88e59f
--- /dev/null
+++ b/app/routes/api.chat.ts
@@ -0,0 +1 @@
+export { action } from './app.chat';
From bfdd884975e966d4537905dd318d9916fb433886 Mon Sep 17 00:00:00 2001
From: Mike Robinson
Date: Mon, 29 Sep 2025 19:01:19 +1300
Subject: [PATCH 10/18] Handle missing Anthropic API key in chat action
---
app/lib/.server/llm/stream-text.ts | 2 +-
app/routes/api.chat.ts | 7 +++++++
app/routes/app.chat.ts | 26 ++++++++++++++++++++++----
app/server/run-chat.server.ts | 4 +++-
4 files changed, 33 insertions(+), 6 deletions(-)
create mode 100644 app/routes/api.chat.ts
diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts
index cf937fd00e..8433e19826 100644
--- a/app/lib/.server/llm/stream-text.ts
+++ b/app/lib/.server/llm/stream-text.ts
@@ -12,7 +12,7 @@ interface ToolResult {
}
interface Message {
- role: 'user' | 'assistant';
+ role: 'system' | 'user' | 'assistant';
content: string;
toolInvocations?: ToolResult[];
}
diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts
new file mode 100644
index 0000000000..aba8233fe3
--- /dev/null
+++ b/app/routes/api.chat.ts
@@ -0,0 +1,7 @@
+export { action } from "./app.chat";
+
+export const loader = () =>
+ new Response("Method Not Allowed", {
+ status: 405,
+ headers: { Allow: "POST" },
+ });
diff --git a/app/routes/app.chat.ts b/app/routes/app.chat.ts
index d10c048c4c..76eff20895 100644
--- a/app/routes/app.chat.ts
+++ b/app/routes/app.chat.ts
@@ -2,6 +2,7 @@ type ChatMessage = { role: "system" | "user" | "assistant"; content: string };
// app/routes/app.chat.ts (or app-chat.ts)
import type { ActionFunctionArgs } from "@remix-run/cloudflare";
+import { env as processEnv } from "node:process";
import { runChat } from "../server/run-chat.server"; // << relative import
export async function action({ request, context }: ActionFunctionArgs) {
@@ -30,11 +31,28 @@ export async function action({ request, context }: ActionFunctionArgs) {
{}
) as Record;
- const stream = await runChat(messages, env);
+ const apiKey =
+ (typeof env.ANTHROPIC_API_KEY === "string" && env.ANTHROPIC_API_KEY) ||
+ processEnv.ANTHROPIC_API_KEY;
- return new Response(stream, {
- headers: { "Content-Type": "text/plain; charset=utf-8" },
- });
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
+ console.error("ANTHROPIC_API_KEY is not configured");
+ return new Response("Server is not configured", { status: 500 });
+ }
+
+ try {
+ const stream = await runChat(messages, {
+ ...env,
+ ANTHROPIC_API_KEY: apiKey,
+ });
+
+ return new Response(stream, {
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
+ });
+ } catch (error) {
+ console.error("Failed to run chat", error);
+ return new Response("There was an error processing your request", { status: 500 });
+ }
}
diff --git a/app/server/run-chat.server.ts b/app/server/run-chat.server.ts
index 6e579243a3..969f931983 100644
--- a/app/server/run-chat.server.ts
+++ b/app/server/run-chat.server.ts
@@ -3,7 +3,9 @@ import { CONTINUE_PROMPT } from "~/lib/.server/llm/prompts";
import { streamText, type Messages, type StreamingOptions } from "~/lib/.server/llm/stream-text";
import SwitchableStream from "~/lib/.server/llm/switchable-stream";
-type Env = Record;
+type Env = Record & {
+ ANTHROPIC_API_KEY: string;
+};
export async function runChat(
messages: Messages,
From fdb131b811fba12411418159d7b0c721185a60e6 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 06:12:46 +0000
Subject: [PATCH 11/18] fix(remix): resolve hydration error by wrapping app in
layout
The root App component was missing the Layout wrapper, causing a mismatch between the server-rendered and client-rendered HTML. This resulted in a React hydration error.
This change wraps the Outlet in the Layout component to ensure the base HTML structure is consistent, resolving the hydration failure.
---
app/root.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/root.tsx b/app/root.tsx
index bd3770f369..157c456980 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -32,7 +32,11 @@ export const links: LinksFunction = () => [
// π This is what entry.server.tsx expects to exist
export default function App() {
- return ;
+ return (
+
+
+
+ );
}
export function Layout({ children }: { children: ReactNode }) {
From 06b8235605d1bf41468a0a0a79a4364affcc98a1 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 10:36:20 +0000
Subject: [PATCH 12/18] fix(remix): resolve hydration error by wrapping app in
layout
The root App component was missing the Layout wrapper, causing a mismatch between the server-rendered and client-rendered HTML. This resulted in a React hydration error.
This change wraps the Outlet in the Layout component to ensure the base HTML structure is consistent, resolving the hydration failure.
From a60ba1b9f995ed5640b546f0181b15cfcbafd149 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 10:57:25 +0000
Subject: [PATCH 13/18] fix(remix): resolve hydration error by wrapping app in
layout
The root App component was missing the Layout wrapper, causing a mismatch between the server-rendered and client-rendered HTML. This resulted in a React hydration error.
This change wraps the Outlet in the Layout component to ensure the base HTML structure is consistent, resolving the hydration failure.
From a6b60e3d13f8900f8c9950323cf032f72e08bf5a Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:03:33 +0000
Subject: [PATCH 14/18] fix(remix): resolve hydration error by wrapping app in
layout
The root App component was missing the Layout wrapper, causing a mismatch between the server-rendered and client-rendered HTML. This resulted in a React hydration error.
This change wraps the Outlet in the Layout component to ensure the base HTML structure is consistent, resolving the hydration failure.
From 9cd96ffcc9d7d0611cd3e813daf056a47880e5fa Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:12:17 +0000
Subject: [PATCH 15/18] fix(remix): resolve hydration error by wrapping app in
layout
The root App component was missing the Layout wrapper, causing a mismatch between the server-rendered and client-rendered HTML. This resulted in a React hydration error.
This change wraps the Outlet in the Layout component to ensure the base HTML structure is consistent, resolving the hydration failure.
From f27518aad73392811da2e93e06748fbffb634049 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:22:13 +0000
Subject: [PATCH 16/18] fix(remix): resolve hydration error by wrapping app in
layout
The root App component was missing the Layout wrapper, causing a mismatch between the server-rendered and client-rendered HTML. This resulted in a React hydration error.
This change wraps the Outlet in the Layout component to ensure the base HTML structure is consistent, resolving the hydration failure.
From 27188cba81ddceb7883704d2ff2d5ae2f6d7a39a Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:36:06 +0000
Subject: [PATCH 17/18] fix(remix): resolve hydration error by wrapping app in
layout
The root App component was missing the Layout wrapper, causing a mismatch between the server-rendered and client-rendered HTML. This resulted in a React hydration error.
This change wraps the Outlet in the Layout component to ensure the base HTML structure is consistent, resolving the hydration failure.
From 9580968afcd1d0104953a071dab8aa280bc68454 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:45:10 +0000
Subject: [PATCH 18/18] fix(remix): resolve hydration error by wrapping app in
layout
The root App component was missing the Layout wrapper, causing a mismatch between the server-rendered and client-rendered HTML. This resulted in a React hydration error.
This change wraps the Outlet in the Layout component to ensure the base HTML structure is consistent, resolving the hydration failure.