fix: remove monorepo
This commit is contained in:
23
app/routes/_index.tsx
Normal file
23
app/routes/_index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { json, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflare';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
import { BaseChat } from '~/components/chat/BaseChat';
|
||||
import { Chat } from '~/components/chat/Chat.client';
|
||||
import { Header } from '~/components/header/Header';
|
||||
import { loadWithAuth } from '~/lib/.server/auth';
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
|
||||
};
|
||||
|
||||
export async function loader(args: LoaderFunctionArgs) {
|
||||
return loadWithAuth(args, async (_args, session) => json({ avatar: session.avatar }));
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<Header />
|
||||
<ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
app/routes/api.chat.ts
Normal file
60
app/routes/api.chat.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { actionWithAuth } from '~/lib/.server/auth';
|
||||
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
|
||||
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';
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
return actionWithAuth(args, chatAction);
|
||||
}
|
||||
|
||||
async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
const { messages } = await request.json<{ messages: Messages }>();
|
||||
|
||||
const stream = new SwitchableStream();
|
||||
|
||||
try {
|
||||
const options: StreamingOptions = {
|
||||
toolChoice: 'none',
|
||||
onFinish: async ({ text: content, finishReason }) => {
|
||||
if (finishReason !== 'length') {
|
||||
return stream.close();
|
||||
}
|
||||
|
||||
if (stream.switches >= MAX_RESPONSE_SEGMENTS) {
|
||||
throw Error('Cannot continue message: Maximum segments reached');
|
||||
}
|
||||
|
||||
const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches;
|
||||
|
||||
console.log(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);
|
||||
|
||||
messages.push({ role: 'assistant', content });
|
||||
messages.push({ role: 'user', content: CONTINUE_PROMPT });
|
||||
|
||||
const result = await streamText(messages, context.cloudflare.env, options);
|
||||
|
||||
return stream.switchSource(result.toAIStream());
|
||||
},
|
||||
};
|
||||
|
||||
const result = await streamText(messages, context.cloudflare.env, options);
|
||||
|
||||
stream.switchSource(result.toAIStream());
|
||||
|
||||
return new Response(stream.readable, {
|
||||
status: 200,
|
||||
headers: {
|
||||
contentType: 'text/plain; charset=utf-8',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
});
|
||||
}
|
||||
}
|
||||
61
app/routes/api.enhancer.ts
Normal file
61
app/routes/api.enhancer.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { StreamingTextResponse, parseStreamPart } from 'ai';
|
||||
import { actionWithAuth } from '~/lib/.server/auth';
|
||||
import { streamText } from '~/lib/.server/llm/stream-text';
|
||||
import { stripIndents } from '~/utils/stripIndent';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
return actionWithAuth(args, enhancerAction);
|
||||
}
|
||||
|
||||
async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||
const { message } = await request.json<{ message: string }>();
|
||||
|
||||
try {
|
||||
const result = await streamText(
|
||||
[
|
||||
{
|
||||
role: 'user',
|
||||
content: stripIndents`
|
||||
I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
|
||||
|
||||
IMPORTANT: Only respond with the improved prompt and nothing else!
|
||||
|
||||
<original_prompt>
|
||||
${message}
|
||||
</original_prompt>
|
||||
`,
|
||||
},
|
||||
],
|
||||
context.cloudflare.env,
|
||||
);
|
||||
|
||||
const transformStream = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
const processedChunk = decoder
|
||||
.decode(chunk)
|
||||
.split('\n')
|
||||
.filter((line) => line !== '')
|
||||
.map(parseStreamPart)
|
||||
.map((part) => part.value)
|
||||
.join('');
|
||||
|
||||
controller.enqueue(encoder.encode(processedChunk));
|
||||
},
|
||||
});
|
||||
|
||||
const transformedStream = result.toAIStream().pipeThrough(transformStream);
|
||||
|
||||
return new StreamingTextResponse(transformedStream);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
});
|
||||
}
|
||||
}
|
||||
9
app/routes/chat.$id.tsx
Normal file
9
app/routes/chat.$id.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { default as IndexRoute } from './_index';
|
||||
import { loadWithAuth } from '~/lib/.server/auth';
|
||||
|
||||
export async function loader(args: LoaderFunctionArgs) {
|
||||
return loadWithAuth(args, async (_args, session) => json({ id: args.params.id, avatar: session.avatar }));
|
||||
}
|
||||
|
||||
export default IndexRoute;
|
||||
201
app/routes/login.tsx
Normal file
201
app/routes/login.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
json,
|
||||
redirect,
|
||||
redirectDocument,
|
||||
type ActionFunctionArgs,
|
||||
type LoaderFunctionArgs,
|
||||
} from '@remix-run/cloudflare';
|
||||
import { useFetcher, useLoaderData } from '@remix-run/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LoadingDots } from '~/components/ui/LoadingDots';
|
||||
import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions';
|
||||
import { identifyUser } from '~/lib/analytics';
|
||||
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
||||
import { request as doRequest } from '~/lib/fetch';
|
||||
import { auth, type AuthAPI } from '~/lib/webcontainer/auth.client';
|
||||
import { logger } from '~/utils/logger';
|
||||
|
||||
export async function loader({ request, context }: LoaderFunctionArgs) {
|
||||
const { session, response } = await isAuthenticated(request, context.cloudflare.env);
|
||||
|
||||
if (session != null) {
|
||||
return redirect('/', response);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
return json(
|
||||
{
|
||||
redirected: url.searchParams.has('code') || url.searchParams.has('error'),
|
||||
},
|
||||
response,
|
||||
);
|
||||
}
|
||||
|
||||
export async function action({ request, context }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
|
||||
const payload = {
|
||||
access: String(formData.get('access')),
|
||||
refresh: String(formData.get('refresh')),
|
||||
};
|
||||
|
||||
let response: Awaited<ReturnType<typeof doRequest>> | undefined;
|
||||
|
||||
try {
|
||||
response = await doRequest(`${CLIENT_ORIGIN}/oauth/token/info`, {
|
||||
headers: { authorization: `Bearer ${payload.access}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await response.json();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Authentication failed');
|
||||
logger.warn(error);
|
||||
|
||||
return json({ error: 'invalid-token' as const }, { status: 401 });
|
||||
}
|
||||
|
||||
const boltEnabled = validateAccessToken(payload.access);
|
||||
|
||||
if (!boltEnabled) {
|
||||
return json({ error: 'bolt-access' as const }, { status: 401 });
|
||||
}
|
||||
|
||||
const identity = await identifyUser(payload.access);
|
||||
|
||||
const tokenInfo: { expires_in: number; created_at: number } = await response.json();
|
||||
|
||||
const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo }, identity);
|
||||
|
||||
return redirectDocument('/', init);
|
||||
}
|
||||
|
||||
type LoginState =
|
||||
| {
|
||||
kind: 'error';
|
||||
error: string;
|
||||
description: string;
|
||||
}
|
||||
| { kind: 'pending' };
|
||||
|
||||
const ERRORS = {
|
||||
'bolt-access': 'You do not have access to Bolt.',
|
||||
'invalid-token': 'Authentication failed.',
|
||||
};
|
||||
|
||||
export default function Login() {
|
||||
const { redirected } = useLoaderData<typeof loader>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!import.meta.hot?.data.wcAuth) {
|
||||
auth.init({ clientId: CLIENT_ID, scope: 'public', editorOrigin: CLIENT_ORIGIN });
|
||||
}
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.wcAuth = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
{redirected ? (
|
||||
<LoadingDots text="Authenticating" />
|
||||
) : (
|
||||
<div className="max-w-md w-full space-y-8 p-10 bg-white rounded-lg shadow">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
||||
</div>
|
||||
<LoginForm />
|
||||
<p className="mt-4 text-sm text-center text-gray-600">
|
||||
By using Bolt, you agree to the collection of usage data for analytics.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginForm() {
|
||||
const [login, setLogin] = useState<LoginState | null>(null);
|
||||
|
||||
const fetcher = useFetcher<typeof action>();
|
||||
|
||||
useEffect(() => {
|
||||
auth.logout({ ignoreRevokeError: true });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetcher.data?.error) {
|
||||
auth.logout({ ignoreRevokeError: true });
|
||||
|
||||
setLogin({
|
||||
kind: 'error' as const,
|
||||
...{ error: fetcher.data.error, description: ERRORS[fetcher.data.error] },
|
||||
});
|
||||
}
|
||||
}, [fetcher.data]);
|
||||
|
||||
async function attemptLogin() {
|
||||
startAuthFlow();
|
||||
|
||||
function startAuthFlow() {
|
||||
auth.startAuthFlow({ popup: true });
|
||||
|
||||
Promise.race([authEvent(auth, 'auth-failed'), auth.loggedIn()]).then((error) => {
|
||||
if (error) {
|
||||
setLogin({ kind: 'error', ...error });
|
||||
} else {
|
||||
onTokens();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function onTokens() {
|
||||
const tokens = auth.tokens()!;
|
||||
|
||||
fetcher.submit(tokens, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
setLogin({ kind: 'pending' });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="w-full text-white bg-accent-600 hover:bg-accent-700 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"
|
||||
onClick={attemptLogin}
|
||||
disabled={login?.kind === 'pending'}
|
||||
>
|
||||
{login?.kind === 'pending' ? 'Authenticating...' : 'Continue with StackBlitz'}
|
||||
</button>
|
||||
{login?.kind === 'error' && (
|
||||
<div>
|
||||
<h2>
|
||||
<code>{login.error}</code>
|
||||
</h2>
|
||||
<p>{login.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface AuthError {
|
||||
error: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function authEvent(auth: AuthAPI, event: 'logged-out'): Promise<void>;
|
||||
function authEvent(auth: AuthAPI, event: 'auth-failed'): Promise<AuthError>;
|
||||
function authEvent(auth: AuthAPI, event: 'logged-out' | 'auth-failed') {
|
||||
return new Promise((resolve) => {
|
||||
const unsubscribe = auth.on(event as any, (arg: any) => {
|
||||
unsubscribe();
|
||||
resolve(arg);
|
||||
});
|
||||
});
|
||||
}
|
||||
10
app/routes/logout.tsx
Normal file
10
app/routes/logout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { logout } from '~/lib/.server/sessions';
|
||||
|
||||
export async function loader({ request, context }: LoaderFunctionArgs) {
|
||||
return logout(request, context.cloudflare.env);
|
||||
}
|
||||
|
||||
export default function Logout() {
|
||||
return '';
|
||||
}
|
||||
Reference in New Issue
Block a user