| import React from 'react' | import React from 'react' | ||||
| import { getDictionary } from '@/i18n/server' | |||||
| import { type Locale } from '@/i18n' | import { type Locale } from '@/i18n' | ||||
| import DevelopMain from '@/app/components/develop' | import DevelopMain from '@/app/components/develop' | ||||
| } | } | ||||
| const Develop = async ({ | const Develop = async ({ | ||||
| params: { locale, appId }, | |||||
| params: { appId }, | |||||
| }: IDevelopProps) => { | }: IDevelopProps) => { | ||||
| const dictionary = await getDictionary(locale) | |||||
| return <DevelopMain appId={appId} dictionary={dictionary} /> | |||||
| return <DevelopMain appId={appId} /> | |||||
| } | } | ||||
| export default Develop | export default Develop |
| type IDevelopMainProps = { | type IDevelopMainProps = { | ||||
| appId: string | appId: string | ||||
| dictionary: any | |||||
| } | } | ||||
| const DevelopMain = ({ appId, dictionary }: IDevelopMainProps) => { | |||||
| const DevelopMain = ({ appId }: IDevelopMainProps) => { | |||||
| const commonParams = { url: '/apps', id: appId } | const commonParams = { url: '/apps', id: appId } | ||||
| const { data: appDetail } = useSWR(commonParams, fetchAppDetail) | const { data: appDetail } = useSWR(commonParams, fetchAppDetail) | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| // const serverApi = `${appDetail?.site?.app_base_url}/api/${appDetail?.site?.access_token}` | |||||
| return ( | return ( | ||||
| <div className='relative flex flex-col h-full overflow-hidden'> | <div className='relative flex flex-col h-full overflow-hidden'> | ||||
| <div className='flex items-center justify-between flex-shrink-0 px-6 border-b border-solid py-2 border-b-gray-100'> | <div className='flex items-center justify-between flex-shrink-0 px-6 border-b border-solid py-2 border-b-gray-100'> | ||||
| <div className='text-lg font-medium text-gray-900'>{dictionary.app?.develop?.title}</div> | |||||
| <div className='text-lg font-medium text-gray-900'></div> | |||||
| <div className='flex items-center flex-wrap gap-y-1'> | <div className='flex items-center flex-wrap gap-y-1'> | ||||
| <InputCopy className='flex-shrink-0 mr-1 w-52 sm:w-80' value={appDetail?.api_base_url}> | <InputCopy className='flex-shrink-0 mr-1 w-52 sm:w-80' value={appDetail?.api_base_url}> | ||||
| <div className={`ml-2 border border-gray-200 border-solid flex-shrink-0 px-2 py-0.5 rounded-[6px] text-gray-500 text-[0.625rem] ${s.customApi}`}> | <div className={`ml-2 border border-gray-200 border-solid flex-shrink-0 px-2 py-0.5 rounded-[6px] text-gray-500 text-[0.625rem] ${s.customApi}`}> |
| import React from 'react' | import React from 'react' | ||||
| import I18N from './i18n' | import I18N from './i18n' | ||||
| import { ToastProvider } from './base/toast' | import { ToastProvider } from './base/toast' | ||||
| import { getDictionary, getLocaleOnServer } from '@/i18n/server' | |||||
| import { getLocaleOnServer } from '@/i18n/server' | |||||
| export type II18NServerProps = { | export type II18NServerProps = { | ||||
| children: React.ReactNode | children: React.ReactNode | ||||
| children, | children, | ||||
| }: II18NServerProps) => { | }: II18NServerProps) => { | ||||
| const locale = getLocaleOnServer() | const locale = getLocaleOnServer() | ||||
| const dictionary = await getDictionary(locale) | |||||
| return ( | return ( | ||||
| <I18N {...{ locale, dictionary }}> | |||||
| <I18N {...{ locale }}> | |||||
| <ToastProvider>{children}</ToastProvider> | <ToastProvider>{children}</ToastProvider> | ||||
| </I18N> | </I18N> | ||||
| ) | ) |
| export type II18nProps = { | export type II18nProps = { | ||||
| locale: Locale | locale: Locale | ||||
| dictionary: Record<string, any> | |||||
| children: React.ReactNode | children: React.ReactNode | ||||
| } | } | ||||
| const I18n: FC<II18nProps> = ({ | const I18n: FC<II18nProps> = ({ | ||||
| locale, | locale, | ||||
| dictionary, | |||||
| children, | children, | ||||
| }) => { | }) => { | ||||
| useEffect(() => { | useEffect(() => { | ||||
| return ( | return ( | ||||
| <I18NContext.Provider value={{ | <I18NContext.Provider value={{ | ||||
| locale, | locale, | ||||
| i18n: dictionary, | |||||
| i18n: {}, | |||||
| setLocaleOnClient, | setLocaleOnClient, | ||||
| }}> | }}> | ||||
| {children} | {children} |
| 'use client' | |||||
| import React, { useState } from 'react' | |||||
| import useSWR from 'swr' | |||||
| import { | |||||
| ChevronDownIcon, | |||||
| ChevronUpIcon, | |||||
| } from '@heroicons/react/24/outline' | |||||
| import { fetchHistories } from '@/models/history' | |||||
| import type { History as HistoryItem } from '@/models/history' | |||||
| import Loading from '@/app/components/base/loading' | |||||
| import { mockAPI } from '@/test/test_util' | |||||
| mockAPI() | |||||
| export type IHistoryProps = { | |||||
| dictionary: any | |||||
| } | |||||
| const HistoryCard = ( | |||||
| { history }: { history: HistoryItem }, | |||||
| ) => { | |||||
| return ( | |||||
| <div className='p-4 h-32 bg-gray-50 border-gray-200 rounded-lg relative flex flex-col justify-between items-center cursor-pointer'> | |||||
| <div className='text-gray-700 text-sm'> | |||||
| {history.source} | |||||
| </div> | |||||
| <div className="absolute inset-0 flex items-center m-4" aria-hidden="true"> | |||||
| <div className="w-full border-t border-gray-100" /> | |||||
| </div> | |||||
| <div className='text-gray-700 text-sm'> | |||||
| {history.target} | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| const History = ({ | |||||
| dictionary, | |||||
| }: IHistoryProps) => { | |||||
| const { data, error } = useSWR('http://localhost:3000/api/histories', fetchHistories) | |||||
| const [showHistory, setShowHistory] = useState(false) | |||||
| const DivideLine = () => { | |||||
| return <div className="mt-6 relative"> | |||||
| {/* divider line */} | |||||
| <div className="absolute inset-0 flex items-center" aria-hidden="true"> | |||||
| <div className="w-full border-t border-gray-300" /> | |||||
| </div> | |||||
| <div className="relative flex justify-center flex-col items-center"> | |||||
| {!showHistory ? <ChevronUpIcon className="h-3 w-3 text-gray-500" aria-hidden="true" /> : <div className='h-3 w-3' />} | |||||
| <span className="px-2 bg-white text-sm font-medium text-gray-600 cursor-pointer">{dictionary.app.textGeneration.history}</span> | |||||
| {!showHistory ? <div className='h-3 w-3' /> : <ChevronDownIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />} | |||||
| </div> | |||||
| </div> | |||||
| } | |||||
| if (error) | |||||
| return <div>failed to load</div> | |||||
| if (!data) | |||||
| return <Loading /> | |||||
| return showHistory | |||||
| ? <div className='w-1/2 block fixed bottom-0 right-0 px-10 py-4' onClick={ | |||||
| () => setShowHistory(v => !v) | |||||
| }> | |||||
| <DivideLine /> | |||||
| <div | |||||
| className='mt-4 grid grid-cols-3 space-x-4 h-[400px] overflow-auto' | |||||
| > | |||||
| {data.histories.map((item: HistoryItem) => | |||||
| <HistoryCard key={item.id} history={item} />)} | |||||
| </div> | |||||
| </div> | |||||
| : <div className='w-1/2 block fixed bottom-0 right-0 px-10 py-4' onClick={ | |||||
| () => setShowHistory(true) | |||||
| }> | |||||
| <DivideLine /> | |||||
| </div> | |||||
| } | |||||
| export default History |
| { | |||||
| "common": { | |||||
| "confrim": "Confirm", | |||||
| "cancel": "Cancel", | |||||
| "refresh": "Refresh" | |||||
| }, | |||||
| "index": { | |||||
| "welcome": "Welcome to " | |||||
| }, | |||||
| "signin": {}, | |||||
| "app": { | |||||
| "overview": { | |||||
| "title": "Overview", | |||||
| "To get started,": "To get started,", | |||||
| "enter your OpenAI API key below": "enter your OpenAI API key below", | |||||
| "Get your API key from OpenAI dashboard": "Get your API key from OpenAI dashboard", | |||||
| "Token Usage": "Token Usage" | |||||
| }, | |||||
| "logs": { | |||||
| "title": "Logs", | |||||
| "description": "You can review and annotate the conversation and response text of the LLM, which will be used for subsequent model fine-tuning." | |||||
| }, | |||||
| "textGeneration": { | |||||
| "history": "History" | |||||
| } | |||||
| } | |||||
| } |
| { | |||||
| "common": { | |||||
| "confrim": "确定", | |||||
| "cancel": "取消", | |||||
| "refresh": "刷新" | |||||
| }, | |||||
| "index": { | |||||
| "welcome": "欢迎来到 " | |||||
| }, | |||||
| "signin": {}, | |||||
| "app": { | |||||
| "overview": { | |||||
| "title": "概览", | |||||
| "To get started,": "从这里开始", | |||||
| "enter your OpenAI API key below 👇": "输入你的 OpenAI API 密钥👇", | |||||
| "Get your API key from OpenAI dashboard": "去 OpenAI 管理面板获取", | |||||
| "Token Usage": "Token 消耗" | |||||
| }, | |||||
| "logs": { | |||||
| "title": "日志", | |||||
| "description": "日志记录了应用的运行情况,包括用户的输入和 AI 的回复。" | |||||
| }, | |||||
| "textGeneration": { | |||||
| "history": "历史" | |||||
| } | |||||
| } | |||||
| } |
| const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale | const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale | ||||
| return matchedLocale | return matchedLocale | ||||
| } | } | ||||
| // We enumerate all dictionaries here for better linting and typescript support | |||||
| // We also get the default import for cleaner types | |||||
| const dictionaries = { | |||||
| 'en': () => import('@/dictionaries/en.json').then(module => module.default), | |||||
| 'zh-Hans': () => import('@/dictionaries/zh-Hans.json').then(module => module.default), | |||||
| } as { [locale: string]: () => Promise<any> } | |||||
| export const getDictionary = async (locale: Locale = 'en') => { | |||||
| try { | |||||
| return await dictionaries[locale]() | |||||
| } | |||||
| catch (e) { console.error('locale not found', locale) } | |||||
| } |
| import { match } from '@formatjs/intl-localematcher' | |||||
| import Negotiator from 'negotiator' | |||||
| import { NextResponse } from 'next/server' | |||||
| import type { NextRequest } from 'next/server' | |||||
| import type { Locale } from './i18n' | |||||
| import { i18n } from './i18n' | |||||
| export const getLocale = (request: NextRequest): Locale => { | |||||
| // @ts-expect-error locales are readonly | |||||
| const locales: Locale[] = i18n.locales | |||||
| let languages: string[] | undefined | |||||
| // get locale from cookie | |||||
| const localeCookie = request.cookies.get('locale') | |||||
| languages = localeCookie?.value ? [localeCookie.value] : [] | |||||
| if (!languages.length) { | |||||
| // Negotiator expects plain object so we need to transform headers | |||||
| const negotiatorHeaders: Record<string, string> = {} | |||||
| request.headers.forEach((value, key) => (negotiatorHeaders[key] = value)) | |||||
| // Use negotiator and intl-localematcher to get best locale | |||||
| languages = new Negotiator({ headers: negotiatorHeaders }).languages() | |||||
| } | |||||
| // match locale | |||||
| let matchedLocale: Locale = i18n.defaultLocale | |||||
| try { | |||||
| // If languages is ['*'], Error would happen in match function. | |||||
| matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale | |||||
| } | |||||
| catch (e) {} | |||||
| return matchedLocale | |||||
| } | |||||
| export const middleware = async (request: NextRequest) => { | |||||
| const pathname = request.nextUrl.pathname | |||||
| if (/\.(css|js(on)?|ico|svg|png)$/.test(pathname)) | |||||
| return | |||||
| const locale = getLocale(request) | |||||
| const response = NextResponse.next() | |||||
| response.cookies.set('locale', locale) | |||||
| return response | |||||
| } |
| export type History = { | |||||
| id: string | |||||
| source: string | |||||
| target: string | |||||
| } | |||||
| export type HistoryResponse = { | |||||
| histories: History[] | |||||
| } | |||||
| export const fetchHistories = (url: string) => | |||||
| fetch(url).then<HistoryResponse>(r => r.json()) |
| import { Factory } from 'miragejs' | import { Factory } from 'miragejs' | ||||
| import { faker } from '@faker-js/faker' | import { faker } from '@faker-js/faker' | ||||
| import type { History } from '@/models/history' | |||||
| import type { User } from '@/models/user' | import type { User } from '@/models/user' | ||||
| import type { Log } from '@/models/log' | |||||
| export const seedHistory = () => { | |||||
| return Factory.extend<Partial<History>>({ | |||||
| source() { | |||||
| return faker.address.streetAddress() | |||||
| }, | |||||
| target() { | |||||
| return faker.address.streetAddress() | |||||
| }, | |||||
| }) | |||||
| } | |||||
| export const seedUser = () => { | export const seedUser = () => { | ||||
| return Factory.extend<Partial<User>>({ | return Factory.extend<Partial<User>>({ | ||||
| }, | }, | ||||
| }) | }) | ||||
| } | } | ||||
| export const seedLog = () => { | |||||
| return Factory.extend<Partial<Log>>({ | |||||
| get key() { | |||||
| return faker.datatype.uuid() | |||||
| }, | |||||
| get conversationId() { | |||||
| return faker.datatype.uuid() | |||||
| }, | |||||
| get question() { | |||||
| return faker.lorem.sentence() | |||||
| }, | |||||
| get answer() { | |||||
| return faker.lorem.sentence() | |||||
| }, | |||||
| get userRate() { | |||||
| return faker.datatype.number(5) | |||||
| }, | |||||
| get adminRate() { | |||||
| return faker.datatype.number(5) | |||||
| }, | |||||
| }) | |||||
| } |
| import { Model, createServer } from 'miragejs' | |||||
| import type { User } from '@/models/user' | |||||
| import type { History } from '@/models/history' | |||||
| import type { Log } from '@/models/log' | |||||
| import { seedHistory, seedLog, seedUser } from '@/test/factories' | |||||
| export function mockAPI() { | |||||
| if (process.env.NODE_ENV === 'development') { | |||||
| console.log('in development mode, starting mock server ... ') | |||||
| const server = createServer({ | |||||
| environment: process.env.NODE_ENV, | |||||
| factories: { | |||||
| user: seedUser(), | |||||
| history: seedHistory(), | |||||
| log: seedLog(), | |||||
| }, | |||||
| models: { | |||||
| user: Model.extend<Partial<User>>({}), | |||||
| history: Model.extend<Partial<History>>({}), | |||||
| log: Model.extend<Partial<Log>>({}), | |||||
| }, | |||||
| routes() { | |||||
| this.namespace = '/api' | |||||
| this.get('/users', () => { | |||||
| return this.schema.all('user') | |||||
| }) | |||||
| this.get('/histories', () => { | |||||
| return this.schema.all('history') | |||||
| }) | |||||
| this.get('/logs', () => { | |||||
| return this.schema.all('log') | |||||
| }) | |||||
| }, | |||||
| seeds(server) { | |||||
| server.createList('user', 20) | |||||
| server.createList('history', 50) | |||||
| server.createList('log', 50) | |||||
| }, | |||||
| }) | |||||
| return server | |||||
| } | |||||
| console.log('Not in development mode, not starting mock server ... ') | |||||
| return null | |||||
| } |