Repository: anse-app/chatgpt-demo Branch: main Commit: 03b47a79d901 Files: 58 Total size: 74.1 KB Directory structure: gitextract_m59yg5z4/ ├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report_when_use.yml │ │ ├── bus_report_when_deploying.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── typo.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── build-docker.yml │ ├── lint.yml │ ├── main.yml │ └── sync.yml ├── .gitignore ├── .npmrc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── README.zh-CN.md ├── astro.config.mjs ├── docker-compose.yml ├── hack/ │ ├── docker-entrypoint.sh │ └── docker-env-replace.sh ├── netlify.toml ├── package.json ├── plugins/ │ └── disableBlocks.ts ├── shims.d.ts ├── src/ │ ├── components/ │ │ ├── ErrorMessageItem.tsx │ │ ├── Footer.astro │ │ ├── Generator.tsx │ │ ├── Header.astro │ │ ├── Logo.astro │ │ ├── MessageItem.tsx │ │ ├── SettingsSlider.tsx │ │ ├── Slider.tsx │ │ ├── SystemRoleSettings.tsx │ │ ├── Themetoggle.astro │ │ └── icons/ │ │ ├── Clear.tsx │ │ ├── Env.tsx │ │ ├── Refresh.tsx │ │ └── X.tsx │ ├── env.d.ts │ ├── layouts/ │ │ └── Layout.astro │ ├── message.css │ ├── pages/ │ │ ├── api/ │ │ │ ├── auth.ts │ │ │ └── generate.ts │ │ ├── index.astro │ │ └── password.astro │ ├── slider.css │ ├── types.ts │ └── utils/ │ ├── auth.ts │ └── openAI.ts ├── tsconfig.json ├── unocss.config.ts └── vercel.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ *.md Dockerfile docker-compose.yml LICENSE netlify.toml vercel.json node_modules .vscode ================================================ FILE: .eslintignore ================================================ dist public node_modules .netlify .vercel .github .changeset ================================================ FILE: .eslintrc.js ================================================ module.exports = { extends: ['@evan-yang', 'plugin:astro/recommended'], rules: { 'no-console': ['error', { allow: ['error'] }], 'react/display-name': 'off', 'react-hooks/rules-of-hooks': 'off', '@typescript-eslint/no-use-before-define': 'off', }, overrides: [ { files: ['*.astro'], parser: 'astro-eslint-parser', parserOptions: { parser: '@typescript-eslint/parser', extraFileExtensions: ['.astro'], }, rules: { 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], }, }, { // Define the configuration for ` ================================================ FILE: src/components/icons/Clear.tsx ================================================ export default () => { return ( ) } ================================================ FILE: src/components/icons/Env.tsx ================================================ export default () => { return ( ) } ================================================ FILE: src/components/icons/Refresh.tsx ================================================ export default () => { return ( ) } ================================================ FILE: src/components/icons/X.tsx ================================================ export default () => { return ( ) } ================================================ FILE: src/env.d.ts ================================================ /// interface ImportMetaEnv { readonly OPENAI_API_KEY: string readonly HTTPS_PROXY: string readonly OPENAI_API_BASE_URL: string readonly HEAD_SCRIPTS: string readonly PUBLIC_SECRET_KEY: string readonly SITE_PASSWORD: string readonly OPENAI_API_MODEL: string readonly PUBLIC_MAX_HISTORY_MESSAGES: string } interface ImportMeta { readonly env: ImportMetaEnv } ================================================ FILE: src/layouts/Layout.astro ================================================ --- import { pwaInfo } from 'virtual:pwa-info' export interface Props { title: string; } const { title } = Astro.props; --- {title} { import.meta.env.HEAD_SCRIPTS && } { pwaInfo && } { import.meta.env.PROD && pwaInfo && } ================================================ FILE: src/message.css ================================================ .message pre { background-color: #64748b10; font-size: 0.8rem; padding: 0.4rem 1rem; } .message .hljs { background-color: transparent; } .message table { font-size: 0.8em; } .message table thead tr { background-color: #64748b40; text-align: left; } .message table th, .message table td { padding: 0.6rem 1rem; } .message table tbody tr:last-of-type { border-bottom: 2px solid #64748b40; } ================================================ FILE: src/pages/api/auth.ts ================================================ import type { APIRoute } from 'astro' const realPassword = import.meta.env.SITE_PASSWORD || '' const passList = realPassword.split(',') || [] export const post: APIRoute = async(context) => { const body = await context.request.json() const { pass } = body return new Response(JSON.stringify({ code: (!realPassword || pass === realPassword || passList.includes(pass)) ? 0 : -1, })) } ================================================ FILE: src/pages/api/generate.ts ================================================ // #vercel-disable-blocks import { ProxyAgent, fetch } from 'undici' // #vercel-end import { generatePayload, parseOpenAIStream } from '@/utils/openAI' import { verifySignature } from '@/utils/auth' import type { APIRoute } from 'astro' const apiKey = import.meta.env.OPENAI_API_KEY const httpsProxy = import.meta.env.HTTPS_PROXY const baseUrl = ((import.meta.env.OPENAI_API_BASE_URL) || 'https://api.openai.com').trim().replace(/\/$/, '') const sitePassword = import.meta.env.SITE_PASSWORD || '' const passList = sitePassword.split(',') || [] export const post: APIRoute = async(context) => { const body = await context.request.json() const { sign, time, messages, pass, temperature } = body if (!messages) { return new Response(JSON.stringify({ error: { message: 'No input text.', }, }), { status: 400 }) } if (sitePassword && !(sitePassword === pass || passList.includes(pass))) { return new Response(JSON.stringify({ error: { message: 'Invalid password.', }, }), { status: 401 }) } if (import.meta.env.PROD && !await verifySignature({ t: time, m: messages?.[messages.length - 1]?.content || '' }, sign)) { return new Response(JSON.stringify({ error: { message: 'Invalid signature.', }, }), { status: 401 }) } const initOptions = generatePayload(apiKey, messages, temperature) // #vercel-disable-blocks if (httpsProxy) initOptions.dispatcher = new ProxyAgent(httpsProxy) // #vercel-end // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error const response = await fetch(`${baseUrl}/v1/chat/completions`, initOptions).catch((err: Error) => { console.error(err) return new Response(JSON.stringify({ error: { code: err.name, message: err.message, }, }), { status: 500 }) }) as Response return parseOpenAIStream(response) as Response } ================================================ FILE: src/pages/index.astro ================================================ --- import Layout from '../layouts/Layout.astro' import Header from '../components/Header.astro' import Footer from '../components/Footer.astro' import Generator from '../components/Generator' import '../message.css' import 'katex/dist/katex.min.css' import 'highlight.js/styles/atom-one-dark.css' ---
================================================ FILE: src/pages/password.astro ================================================ --- import Layout from '../layouts/Layout.astro' ---
Please input password
================================================ FILE: src/slider.css ================================================ /* ----------------------------------------------------------------------------- * Slider * -----------------------------------------------------------------------------*/ [data-scope='slider'][data-part='root'] { @apply w-full flex flex-col } [data-scope='slider'][data-part='root'][data-orientation='vertical'] { @apply h-60 } [data-scope='slider'][data-part='control'] { --slider-thumb-size: 14px; --slider-track-height: 4px; @apply relative fcc cursor-pointer } [data-scope='slider'][data-part='control'][data-orientation='horizontal'] { @apply h-[var(--slider-thumb-size)]; } [data-scope='slider'][data-part='control'][data-orientation='vertical'] { @apply w-[var(--slider-thumb-size)]; } [data-scope='slider'][data-part='control']:hover [data-part='range'] { @apply bg-gray-400 dark:bg-gray-600 } [data-scope='slider'][data-part='control']:hover [data-part='thumb'] { @apply bg-gray-300 dark:bg-gray-400 } [data-scope='slider'][data-part='thumb'] { all: unset; @apply bg-gray-200 dark:bg-gray-500 w-[var(--slider-thumb-size)] h-[var(--slider-thumb-size)] rounded-full b-#c5c5d2 b-2 } [data-scope='slider'][data-part='thumb'][data-disabled] { @apply w-0 } [data-scope='slider'] .control-area { @apply flex mt-12px } .slider [data-orientation='horizontal'] .control-area { flex-direction: column; width: 100%; } .slider [data-orientation='vertical'] .control-area { flex-direction: row; height: 100%; } [data-scope='slider'][data-part='track'] { @apply rounded-full bg-gray-200 dark:bg-neutral-700 } [data-scope='slider'][data-part='track'][data-orientation='horizontal'] { @apply h-[var(--slider-track-height)] w-full; } [data-scope='slider'][data-part='track'][data-orientation='vertical'] { @apply h-full w-[var(--slider-track-height)]; } [data-scope='slider'][data-part='range'] { @apply bg-neutral-300 dark:bg-gray-700 } [data-scope='slider'][data-part='range'][data-disabled] { @apply bg-neutral-300 dark:bg-gray-600 } [data-scope='slider'][data-part='range'][data-orientation='horizontal'] { @apply h-full; } [data-scope='slider'][data-part='range'][data-orientation='vertical'] { @apply w-full; } [data-scope='slider'][data-part='output'] { margin-inline-start: 12px; } [data-scope='slider'][data-part='marker'] { color: lightgray; } ================================================ FILE: src/types.ts ================================================ export interface ChatMessage { role: 'system' | 'user' | 'assistant' content: string } export interface ErrorMessage { code: string message: string } ================================================ FILE: src/utils/auth.ts ================================================ import { sha256 } from 'js-sha256' interface AuthPayload { t: number m: string } async function digestMessage(message: string) { if (typeof crypto !== 'undefined' && crypto?.subtle?.digest) { const msgUint8 = new TextEncoder().encode(message) const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') } else { return sha256(message).toString() } } export const generateSignature = async(payload: AuthPayload) => { const { t: timestamp, m: lastMessage } = payload const secretKey = import.meta.env.PUBLIC_SECRET_KEY as string || '' const signText = `${timestamp}:${lastMessage}:${secretKey}` // eslint-disable-next-line no-return-await return await digestMessage(signText) } export const verifySignature = async(payload: AuthPayload, sign: string) => { // if (Math.abs(payload.t - Date.now()) > 1000 * 60 * 5) { // return false // } const payloadSign = await generateSignature(payload) return payloadSign === sign } ================================================ FILE: src/utils/openAI.ts ================================================ import { createParser } from 'eventsource-parser' import type { ParsedEvent, ReconnectInterval } from 'eventsource-parser' import type { ChatMessage } from '@/types' export const model = import.meta.env.OPENAI_API_MODEL || 'gpt-3.5-turbo' export const generatePayload = ( apiKey: string, messages: ChatMessage[], temperature: number, ): RequestInit & { dispatcher?: any } => ({ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, method: 'POST', body: JSON.stringify({ model, messages, temperature, stream: true, }), }) export const parseOpenAIStream = (rawResponse: Response) => { const encoder = new TextEncoder() const decoder = new TextDecoder() if (!rawResponse.ok) { return new Response(rawResponse.body, { status: rawResponse.status, statusText: rawResponse.statusText, }) } const stream = new ReadableStream({ async start(controller) { const streamParser = (event: ParsedEvent | ReconnectInterval) => { if (event.type === 'event') { const data = event.data if (data === '[DONE]') { controller.close() return } try { // response = { // id: 'chatcmpl-6pULPSegWhFgi0XQ1DtgA3zTa1WR6', // object: 'chat.completion.chunk', // created: 1677729391, // model: 'gpt-3.5-turbo-0301', // choices: [ // { delta: { content: '你' }, index: 0, finish_reason: null } // ], // } const json = JSON.parse(data) const text = json.choices[0].delta?.content || '' const queue = encoder.encode(text) controller.enqueue(queue) } catch (e) { controller.error(e) } } } const parser = createParser(streamParser) for await (const chunk of rawResponse.body as any) parser.feed(decoder.decode(chunk)) }, }) return new Response(stream) } ================================================ FILE: tsconfig.json ================================================ { "extends": "astro/tsconfigs/base", "compilerOptions": { "baseUrl": ".", "jsx": "preserve", "jsxImportSource": "solid-js", "types": ["vite-plugin-pwa/info"], "paths": { "@/*": ["src/*"], }, } } ================================================ FILE: unocss.config.ts ================================================ import { defineConfig, presetAttributify, presetIcons, presetTypography, presetUno, transformerDirectives, transformerVariantGroup, } from 'unocss' export default defineConfig({ presets: [ presetUno(), presetAttributify(), presetIcons({ scale: 1.1, cdn: 'https://esm.sh/', }), presetTypography({ cssExtend: { 'ul,ol': { 'padding-left': '2.25em', 'position': 'relative', }, }, }), ], transformers: [transformerVariantGroup(), transformerDirectives()], shortcuts: [{ 'fc': 'flex justify-center', 'fi': 'flex items-center', 'fb': 'flex justify-between', 'fcc': 'fc items-center', 'fie': 'fi justify-end', 'col-fcc': 'flex-col fcc', 'inline-fcc': 'inline-flex items-center justify-center', 'base-focus': 'focus:(bg-op-20 ring-0 outline-none)', 'b-slate-link': 'border-b border-(slate none) hover:border-dashed', 'gpt-title': 'text-2xl font-extrabold mr-1', 'gpt-subtitle': 'text-(2xl transparent) font-extrabold bg-(clip-text gradient-to-r) from-sky-400 to-emerald-600', 'gpt-copy-btn': 'absolute top-12px right-12px z-3 fcc border b-transparent w-8 h-8 p-2 bg-light-300 dark:bg-dark-300 op-90 cursor-pointer', 'gpt-copy-tips': 'op-0 h-7 bg-black px-2.5 py-1 box-border text-xs c-white fcc rounded absolute z-1 transition duration-600 whitespace-nowrap -top-8', 'gpt-retry-btn': 'fi gap-1 px-2 py-0.5 op-70 border border-slate rounded-md text-sm cursor-pointer hover:bg-slate/10', 'gpt-back-top-btn': 'fcc p-2.5 text-base rounded-md hover:bg-slate/10 fixed bottom-60px right-20px z-10 cursor-pointer transition-colors', 'gpt-back-bottom-btn': 'gpt-back-top-btn bottom-20px transform-rotate-180deg', 'gpt-password-input': 'px-4 py-3 h-12 rounded-sm bg-(slate op-15) base-focus', 'gpt-password-submit': 'fcc h-12 w-12 bg-slate cursor-pointer bg-op-20 hover:bg-op-50', 'gen-slate-btn': 'h-12 px-4 py-2 bg-(slate op-15) hover:bg-op-20 rounded-sm', 'gen-cb-wrapper': 'h-12 my-4 fcc gap-4 bg-(slate op-15) rounded-sm', 'gen-cb-stop': 'px-2 py-0.5 border border-slate rounded-md text-sm op-70 cursor-pointer hover:bg-slate/10', 'gen-text-wrapper': 'my-4 fc gap-2 transition-opacity', 'gen-textarea': 'w-full px-3 py-3 min-h-12 max-h-36 rounded-sm bg-(slate op-15) resize-none base-focus placeholder:op-50 dark:(placeholder:op-30) scroll-pa-8px', 'sys-edit-btn': 'inline-fcc gap-1 text-sm bg-slate/20 px-2 py-1 rounded-md transition-colors cursor-pointer hover:bg-slate/50', 'stick-btn-on': '!bg-$c-fg text-$c-bg hover:op-80', }], }) ================================================ FILE: vercel.json ================================================ { "buildCommand": "OUTPUT=vercel astro build" }