-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c3322a3
Showing
76 changed files
with
11,121 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
node_modules | ||
|
||
/.cache | ||
/build | ||
.env | ||
.react-router |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
node_modules | ||
|
||
/.cache | ||
/build | ||
.env | ||
.react-router | ||
fly.toml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
ARG NODE_VERSION=20.11.0 | ||
FROM node:${NODE_VERSION}-slim as base | ||
|
||
# Node.js app lives here | ||
WORKDIR /app | ||
|
||
# Set production environment | ||
ENV NODE_ENV="production" | ||
|
||
# Install pnpm | ||
ARG PNPM_VERSION=9.12.3 | ||
RUN npm install -g pnpm@$PNPM_VERSION | ||
|
||
|
||
# Throw-away build stage to reduce size of final image | ||
FROM base as build | ||
|
||
# Install packages needed to build node modules | ||
RUN apt-get update -qq && \ | ||
apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 | ||
|
||
# Install node modules | ||
COPY package.json pnpm-lock.yaml ./ | ||
RUN pnpm install --frozen-lockfile --prod=false | ||
|
||
# Copy application code | ||
COPY . . | ||
|
||
# Build application | ||
RUN pnpm run build | ||
|
||
# Remove development dependencies | ||
RUN pnpm prune --prod | ||
|
||
|
||
# Final stage for app image | ||
FROM base | ||
|
||
# Copy built application | ||
COPY --from=build /app /app | ||
|
||
# Start the server by default, this can be overwritten at runtime | ||
EXPOSE 3000 | ||
CMD [ "pnpm", "run", "start" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Welcome to React Router! | ||
|
||
- 📖 [React Router docs](https://reactrouter.com/dev) | ||
|
||
## Development | ||
|
||
Run the dev server: | ||
|
||
```shellscript | ||
npm run dev | ||
``` | ||
|
||
## Deployment | ||
|
||
First, build your app for production: | ||
|
||
```sh | ||
npm run build | ||
``` | ||
|
||
Then run the app in production mode: | ||
|
||
```sh | ||
npm start | ||
``` | ||
|
||
Now you'll need to pick a host to deploy it to. | ||
|
||
### DIY | ||
|
||
If you're familiar with deploying Node applications, the built-in app server is production-ready. | ||
|
||
Make sure to deploy the output of `npm run build` | ||
|
||
- `build/server` | ||
- `build/client` | ||
|
||
## Styling | ||
|
||
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { IdbFs, PGlite } from "@electric-sql/pglite"; | ||
import { getItem, setItem } from "localforage"; | ||
import wasm from "node_modules/@electric-sql/pglite/dist/postgres.wasm?url"; | ||
import wasmData from "node_modules/@electric-sql/pglite/dist/postgres.data?url"; | ||
import { drizzle } from "drizzle-orm/pglite"; | ||
import * as s from "~/drizzle/schema"; | ||
|
||
async function getWasm() { | ||
const item = (await getItem("pg_wasm")) as ArrayBuffer; | ||
if (item) { | ||
return await WebAssembly.compile(item); | ||
} | ||
const resp = await fetch(wasm); | ||
const data = await resp.arrayBuffer(); | ||
await setItem("pg_wasm", data); | ||
return await WebAssembly.compile(data); | ||
} | ||
|
||
async function getWasmData() { | ||
const item = (await getItem("pg_data")) as Blob; | ||
if (item) { | ||
return item; | ||
} | ||
const resp = await fetch(wasmData); | ||
const data = await resp.blob(); | ||
await setItem("pg_data", data); | ||
return data; | ||
} | ||
|
||
export const client = new PGlite({ | ||
wasmModule: await getWasm(), | ||
fsBundle: await getWasmData(), | ||
fs: new IdbFs("chatbot"), | ||
}); | ||
|
||
await client.exec(` | ||
CREATE TABLE IF NOT EXISTS "conversations" ( | ||
"id" varchar PRIMARY KEY NOT NULL, | ||
"name" varchar, | ||
"created_at" timestamp DEFAULT now() NOT NULL, | ||
"updated_at" timestamp DEFAULT now() NOT NULL | ||
); | ||
--> statement-breakpoint | ||
CREATE TABLE IF NOT EXISTS "images" ( | ||
"id" varchar PRIMARY KEY NOT NULL, | ||
"url" varchar NOT NULL, | ||
"prompt" varchar NOT NULL, | ||
"created_at" timestamp DEFAULT now() NOT NULL, | ||
"updated_at" timestamp DEFAULT now() NOT NULL | ||
); | ||
--> statement-breakpoint | ||
CREATE TABLE IF NOT EXISTS "messages" ( | ||
"id" varchar PRIMARY KEY NOT NULL, | ||
"conversation_id" varchar NOT NULL, | ||
"role" varchar NOT NULL, | ||
"content" text NOT NULL, | ||
"created_at" timestamp DEFAULT now() NOT NULL, | ||
"updated_at" timestamp DEFAULT now() NOT NULL | ||
); | ||
`); | ||
|
||
export const schema = s; | ||
export const db = drizzle(client, { | ||
schema, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import ky from "ky"; | ||
|
||
export const BASE_API_URL = "https://api.pexni.com"; | ||
|
||
let isRefreshing = false; | ||
let refreshSubscribers: ((token: string) => void)[] = []; | ||
|
||
function subscribeTokenRefresh(cb: (token: string) => void) { | ||
refreshSubscribers.push(cb); | ||
} | ||
function onRefreshed(token: string) { | ||
for (const subscriber of refreshSubscribers) { | ||
subscriber(token); | ||
} | ||
refreshSubscribers = []; | ||
} | ||
const refreshAccessToken = async (): Promise<string> => { | ||
const refresh_token = localStorage.getItem("refresh_token"); | ||
const resp = await api | ||
.post("refresh_token", { | ||
json: { token: refresh_token }, | ||
}) | ||
.json<SigninResp>(); | ||
localStorage.setItem("access_token", resp.access_token); | ||
localStorage.setItem("refresh_token", resp.refresh_token); | ||
onRefreshed(resp.access_token); | ||
return resp.access_token; | ||
}; | ||
export const api = ky.create({ | ||
prefixUrl: BASE_API_URL, | ||
hooks: { | ||
beforeRequest: [ | ||
(request) => { | ||
const token = localStorage.getItem("access_token"); | ||
if (token) { | ||
request.headers.set("Authorization", `Bearer ${token}`); | ||
} | ||
}, | ||
], | ||
afterResponse: [ | ||
async (request, options, response) => { | ||
if (response.status === 401) { | ||
if (!isRefreshing) { | ||
isRefreshing = true; | ||
try { | ||
const token = await refreshAccessToken(); | ||
request.headers.set("Authorization", `Bearer ${token}`); | ||
return api(request, options); | ||
} catch (error) { | ||
console.error("Failed to refresh token", error); | ||
} finally { | ||
isRefreshing = false; | ||
} | ||
} else { | ||
return new Promise((resolve) => { | ||
subscribeTokenRefresh((token) => { | ||
request.headers.set("Authorization", `Bearer ${token}`); | ||
resolve(api(request, options)); | ||
}); | ||
}); | ||
} | ||
} | ||
}, | ||
], | ||
}, | ||
}); | ||
|
||
interface SigninResp { | ||
access_token: string; | ||
refresh_token: string; | ||
} | ||
|
||
interface User { | ||
id: string; | ||
username: string; | ||
} | ||
|
||
export interface Conversation { | ||
id: string; | ||
name: string; | ||
pinned: boolean; | ||
created_at: string; | ||
updated_at: string; | ||
} | ||
|
||
export interface Message { | ||
id: string; | ||
conversation_id: string; | ||
user_id: string; | ||
role: string; | ||
content: string; | ||
created_at: string; | ||
updated_at: string; | ||
} | ||
|
||
export interface SendMessageData { | ||
role: string; | ||
content: string; | ||
} | ||
|
||
export const signin = (data: { username: string; password: string }) => | ||
api.post("signin", { json: data }).json<SigninResp>(); | ||
|
||
export const signup = (data: { username: string; password: string }) => | ||
api.post("signup", { json: data }).json<User>(); | ||
|
||
export const account = () => api.get("account").json<User>(); | ||
|
||
export const getConversations = () => | ||
api.get("conversations").json<Conversation[]>(); | ||
|
||
export const createConversation = () => | ||
api.post("conversations").json<Conversation>(); | ||
|
||
export const getConversation = (conversationId: string) => | ||
api.get(`conversations/${conversationId}`).json<Conversation>(); | ||
|
||
export const deleteConversation = (conversationId: string) => | ||
api.delete(`conversations/${conversationId}`); | ||
|
||
export const getMessages = (conversationId: string) => | ||
api.get(`conversations/${conversationId}/messages`).json<Message[]>(); | ||
|
||
export const chat = (messages: SendMessageData[]) => | ||
api.post("chat", { | ||
json: { | ||
messages: messages, | ||
model: "@cf/qwen/qwen1.5-14b-chat-awq", | ||
}, | ||
}); | ||
|
||
export const generateImages = (prompt: string) => | ||
api.post("images", { json: { prompt }, timeout: 200000 }).json<{ | ||
image: string; | ||
}>(); | ||
|
||
export const getImageBinary = (key: string) => api.get(`images/${key}`).blob(); | ||
|
||
export const summarize = (messages: SendMessageData[]) => | ||
api | ||
.post("summarize", { | ||
json: { | ||
messages: messages, | ||
}, | ||
}) | ||
.json<string>(); |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import AutoResize from "react-textarea-autosize"; | ||
import { chakra, useRecipe } from "@chakra-ui/react"; | ||
|
||
const StyledAutoResize = chakra(AutoResize); | ||
|
||
type AutoResizedTextareaProps = React.ComponentProps<typeof StyledAutoResize>; | ||
|
||
export function AutoResizedTextarea(props: AutoResizedTextareaProps) { | ||
const recipe = useRecipe({ key: "textarea" }); | ||
const styles = recipe({ size: "sm" }); | ||
return <StyledAutoResize {...props} css={styles} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { HStack, VStack, Text } from "@chakra-ui/react"; | ||
import { BotIcon } from "lucide-react"; | ||
import { memo } from "react"; | ||
import { formatRelative } from "date-fns"; | ||
import { zhCN } from "date-fns/locale"; | ||
import Markdown from "react-markdown"; | ||
import rehypeHighlight from "rehype-highlight"; | ||
import remarkGfm from "remark-gfm"; | ||
import { Avatar } from "~/components/ui/avatar"; | ||
import { Prose } from "~/components/ui/prose"; | ||
import { Message } from "~/drizzle/schema"; | ||
|
||
interface ChatBubbleProps { | ||
message: Message; | ||
} | ||
|
||
export const ChatBubbleMemo = memo(({ message }: ChatBubbleProps) => { | ||
return <ChatBubble message={message} />; | ||
}); | ||
|
||
export function ChatBubble({ message }: ChatBubbleProps) { | ||
return ( | ||
<HStack | ||
w="full" | ||
rounded="sm" | ||
p={2} | ||
alignItems="start" | ||
flexDir={message.role === "user" ? "row-reverse" : "row"} | ||
> | ||
{message.role !== "user" && ( | ||
<Avatar | ||
size="sm" | ||
fallback={<BotIcon size={20} strokeWidth="1.5px" />} | ||
/> | ||
)} | ||
<VStack w="full" alignItems={message.role === "user" ? "end" : "start"}> | ||
<Text textStyle="xs" color="gray"> | ||
{formatRelative(message.createdAt, new Date(), { | ||
locale: zhCN, | ||
})} | ||
</Text> | ||
<Prose | ||
bgColor="gray.100" | ||
color="gray.800" | ||
px={3} | ||
rounded="2xl" | ||
css={{ | ||
"& pre": { | ||
whiteSpace: "pre-wrap", | ||
}, | ||
}} | ||
> | ||
<Markdown | ||
rehypePlugins={[rehypeHighlight]} | ||
remarkPlugins={[remarkGfm]} | ||
> | ||
{message.content} | ||
</Markdown> | ||
</Prose> | ||
</VStack> | ||
</HStack> | ||
); | ||
} |
Oops, something went wrong.