mirror of
https://github.com/langgenius/webapp-text-generator.git
synced 2026-07-01 20:05:07 -04:00
feat: init
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,tsx}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": [
|
||||
"@antfu",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-definitions": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"no-console": "off",
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"flatTernaryExpressions": false,
|
||||
"ignoredNodes": [
|
||||
"PropertyDefinition[decorators]",
|
||||
"TSUnionType",
|
||||
"FunctionExpression[params]:has(Identifier[decorators])"
|
||||
]
|
||||
}
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# npm
|
||||
package-lock.json
|
||||
|
||||
# yarn
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
.yarn/
|
||||
yarn.lock
|
||||
.yarnrc.yml
|
||||
|
||||
# pmpm
|
||||
pnpm-lock.yaml
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run dev"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run dev",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"typescript.tsdk": ".yarn/cache/typescript-patch-72dc6f164f-ab417a2f39.zip/node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"[python]": {
|
||||
"editor.formatOnType": true
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"i18n-ally.localesPaths": [
|
||||
"i18n",
|
||||
"i18n/lang",
|
||||
"app/api/messages"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# Text Generator Web App Template
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Config App
|
||||
Config app in `config/index.ts`.Please config:
|
||||
- APP_ID
|
||||
- API_KEY
|
||||
|
||||
More config:
|
||||
```js
|
||||
export const APP_INFO: AppInfo = {
|
||||
"title": 'Chat APP',
|
||||
"description": '',
|
||||
"copyright": '',
|
||||
"privacy_policy": '',
|
||||
"default_language": 'zh-Hans'
|
||||
}
|
||||
|
||||
export const isShowPrompt = true
|
||||
export const promptTemplate = ''
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
First, install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn
|
||||
# or
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Then, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { OpenAIStream } from '@/app/api/utils/stream'
|
||||
import { getInfo, client } from '@/app/api/utils/common'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const {
|
||||
inputs,
|
||||
query,
|
||||
response_mode: responseMode
|
||||
} = body
|
||||
const { user } = getInfo(request);
|
||||
const res = await client.createCompletionMessage(inputs, query, user, responseMode)
|
||||
const stream = await OpenAIStream(res as any)
|
||||
return new Response(stream as any)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getInfo, client } from '@/app/api/utils/common'
|
||||
|
||||
export async function POST(request: NextRequest, { params }: {
|
||||
params: { messageId: string }
|
||||
}) {
|
||||
const body = await request.json()
|
||||
const {
|
||||
rating
|
||||
} = body
|
||||
const { messageId } = params
|
||||
const { user } = getInfo(request);
|
||||
try {
|
||||
const { data } = await client.messageFeedback(messageId, rating, user)
|
||||
return NextResponse.json(data)
|
||||
} catch (e) {
|
||||
return NextResponse.json(e)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getInfo, setSession, client } from '@/app/api/utils/common'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { sessionId, user } = getInfo(request);
|
||||
const { searchParams } = new URL(request.url);
|
||||
const conversationId = searchParams.get('conversation_id')
|
||||
const { data }: any = await client.getConversationMessages(user, conversationId as string);
|
||||
return NextResponse.json(data, {
|
||||
headers: setSession(sessionId)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getInfo, setSession, client } from '@/app/api/utils/common'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { sessionId, user } = getInfo(request);
|
||||
const { data } = await client.getApplicationParameters(user);
|
||||
return NextResponse.json(data as object, {
|
||||
headers: setSession(sessionId)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { APP_ID, API_KEY } from '@/config'
|
||||
import { CompletionClient } from 'langgenius-client'
|
||||
import uuid from 'uuid'
|
||||
|
||||
const userPrefix = `user_${APP_ID}:`;
|
||||
|
||||
export const getInfo = (request: NextRequest) => {
|
||||
const sessionId = request.cookies.get('session_id')?.value || uuid.v4();
|
||||
const user = userPrefix + sessionId;
|
||||
return {
|
||||
sessionId,
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
export const setSession = (sessionId: string) => {
|
||||
return { 'Set-Cookie': `session_id=${sessionId}` }
|
||||
}
|
||||
|
||||
export const client = new CompletionClient(API_KEY)
|
||||
@@ -0,0 +1,25 @@
|
||||
export async function OpenAIStream(res: { body: any }) {
|
||||
const reader = res.body.getReader();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
|
||||
// https://github.com/whichlight/chatgpt-api-streaming/blob/master/pages/api/OpenAIStream.ts
|
||||
start(controller) {
|
||||
return pump();
|
||||
function pump() {
|
||||
return reader.read().then(({ done, value }: any) => {
|
||||
// When no more data needs to be consumed, close the stream
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Enqueue the next data chunk into our target stream
|
||||
controller.enqueue(value);
|
||||
return pump();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type IAppUnavailableProps = {
|
||||
isUnknwonReason: boolean
|
||||
errMessage?: string
|
||||
}
|
||||
|
||||
const AppUnavailable: FC<IAppUnavailableProps> = ({
|
||||
isUnknwonReason,
|
||||
errMessage,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
let message = errMessage
|
||||
if (!errMessage) {
|
||||
message = (isUnknwonReason ? t('app.common.appUnkonwError') : t('app.common.appUnavailable')) as string
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-center w-screen h-screen'>
|
||||
<h1 className='mr-5 h-[50px] leading-[50px] pr-5 text-[24px] font-medium'
|
||||
style={{
|
||||
borderRight: '1px solid rgba(0,0,0,.3)',
|
||||
}}>{(errMessage || isUnknwonReason) ? 500 : 404}</h1>
|
||||
<div className='text-sm'>{message}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AppUnavailable)
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { FC } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import style from './style.module.css'
|
||||
|
||||
export type AppIconProps = {
|
||||
size?: 'tiny' | 'small' | 'medium' | 'large'
|
||||
rounded?: boolean
|
||||
icon?: string
|
||||
background?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AppIcon: FC<AppIconProps> = ({
|
||||
size = 'medium',
|
||||
rounded = false,
|
||||
background,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
style.appIcon,
|
||||
size !== 'medium' && style[size],
|
||||
rounded && style.rounded,
|
||||
className ?? '',
|
||||
)}
|
||||
style={{
|
||||
background,
|
||||
}}
|
||||
>
|
||||
🤖
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppIcon
|
||||
@@ -0,0 +1,15 @@
|
||||
.appIcon {
|
||||
@apply flex items-center justify-center relative w-9 h-9 text-lg bg-teal-100 rounded-lg grow-0 shrink-0;
|
||||
}
|
||||
.appIcon.large {
|
||||
@apply w-10 h-10;
|
||||
}
|
||||
.appIcon.small {
|
||||
@apply w-8 h-8;
|
||||
}
|
||||
.appIcon.tiny {
|
||||
@apply w-6 h-6 text-base;
|
||||
}
|
||||
.appIcon.rounded {
|
||||
@apply rounded-full;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { FC, MouseEventHandler } from 'react'
|
||||
import React from 'react'
|
||||
import Spinner from '@/app/components/base/spinner'
|
||||
|
||||
export type IButtonProps = {
|
||||
type?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
children: React.ReactNode
|
||||
onClick?: MouseEventHandler<HTMLDivElement>
|
||||
}
|
||||
|
||||
const Button: FC<IButtonProps> = ({
|
||||
type,
|
||||
disabled,
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
loading = false,
|
||||
}) => {
|
||||
let style = 'cursor-pointer'
|
||||
switch (type) {
|
||||
case 'primary':
|
||||
style = (disabled || loading) ? 'bg-primary-600/75 cursor-not-allowed text-white' : 'bg-primary-600 hover:bg-primary-600/75 hover:shadow-md cursor-pointer text-white hover:shadow-sm'
|
||||
break
|
||||
default:
|
||||
style = disabled ? 'border-solid border border-gray-200 bg-gray-200 cursor-not-allowed text-gray-800' : 'border-solid border border-gray-200 cursor-pointer text-gray-500 hover:bg-white hover:shadow-sm hover:border-gray-300'
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center items-center content-center h-9 leading-5 rounded-lg px-4 py-2 text-base ${style} ${className && className}`}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
>
|
||||
{children}
|
||||
{/* Spinner is hidden when loading is false */}
|
||||
<Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Button)
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
|
||||
import './style.css'
|
||||
|
||||
type ILoadingProps = {
|
||||
type?: 'area' | 'app'
|
||||
}
|
||||
const Loading = (
|
||||
{ type = 'area' }: ILoadingProps = { type: 'area' },
|
||||
) => {
|
||||
return (
|
||||
<div className={`flex w-full justify-center items-center ${type === 'app' ? 'h-full' : ''}`}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className='spin-animation'>
|
||||
<g clipPath="url(#clip0_324_2488)">
|
||||
<path d="M15 0H10C9.44772 0 9 0.447715 9 1V6C9 6.55228 9.44772 7 10 7H15C15.5523 7 16 6.55228 16 6V1C16 0.447715 15.5523 0 15 0Z" fill="#1C64F2" />
|
||||
<path opacity="0.5" d="M15 9H10C9.44772 9 9 9.44772 9 10V15C9 15.5523 9.44772 16 10 16H15C15.5523 16 16 15.5523 16 15V10C16 9.44772 15.5523 9 15 9Z" fill="#1C64F2" />
|
||||
<path opacity="0.1" d="M6 9H1C0.447715 9 0 9.44772 0 10V15C0 15.5523 0.447715 16 1 16H6C6.55228 16 7 15.5523 7 15V10C7 9.44772 6.55228 9 6 9Z" fill="#1C64F2" />
|
||||
<path opacity="0.2" d="M6 0H1C0.447715 0 0 0.447715 0 1V6C0 6.55228 0.447715 7 1 7H6C6.55228 7 7 6.55228 7 6V1C7 0.447715 6.55228 0 6 0Z" fill="#1C64F2" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_324_2488">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
||||
@@ -0,0 +1,41 @@
|
||||
.spin-animation path {
|
||||
animation: custom 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes custom {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
25% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.spin-animation path:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.spin-animation path:nth-child(2) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.spin-animation path:nth-child(3) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.spin-animation path:nth-child(4) {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import RemarkMath from 'remark-math'
|
||||
import RemarkBreaks from 'remark-breaks'
|
||||
import RehypeKatex from 'rehype-katex'
|
||||
import RemarkGfm from 'remark-gfm'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter'
|
||||
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
|
||||
|
||||
export function Markdown(props: { content: string }) {
|
||||
return (
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline && match
|
||||
? (
|
||||
<SyntaxHighlighter
|
||||
{...props}
|
||||
children={String(children).replace(/\n$/, '')}
|
||||
style={atelierHeathLight}
|
||||
language={match[1]}
|
||||
showLineNumbers
|
||||
PreTag="div"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<code {...props} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
}}
|
||||
linkTarget={'_blank'}
|
||||
>
|
||||
{props.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import { Combobox, Listbox, Transition } from '@headlessui/react'
|
||||
import classNames from 'classnames'
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
const defaultItems = [
|
||||
{ value: 1, name: 'option1' },
|
||||
{ value: 2, name: 'option2' },
|
||||
{ value: 3, name: 'option3' },
|
||||
{ value: 4, name: 'option4' },
|
||||
{ value: 5, name: 'option5' },
|
||||
{ value: 6, name: 'option6' },
|
||||
{ value: 7, name: 'option7' },
|
||||
]
|
||||
|
||||
export type Item = {
|
||||
value: number | string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ISelectProps = {
|
||||
className?: string
|
||||
items?: Item[]
|
||||
defaultValue?: number | string
|
||||
disabled?: boolean
|
||||
onSelect: (value: Item) => void
|
||||
allowSearch?: boolean
|
||||
bgClassName?: string
|
||||
}
|
||||
const Select: FC<ISelectProps> = ({
|
||||
className,
|
||||
items = defaultItems,
|
||||
defaultValue = 1,
|
||||
disabled = false,
|
||||
onSelect,
|
||||
allowSearch = true,
|
||||
bgClassName = 'bg-gray-100',
|
||||
}) => {
|
||||
const [query, setQuery] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null)
|
||||
useEffect(() => {
|
||||
let defaultSelect = null
|
||||
const existed = items.find((item: Item) => item.value === defaultValue)
|
||||
if (existed)
|
||||
defaultSelect = existed
|
||||
|
||||
setSelectedItem(defaultSelect)
|
||||
}, [defaultValue])
|
||||
|
||||
const filteredItems: Item[]
|
||||
= query === ''
|
||||
? items
|
||||
: items.filter((item) => {
|
||||
return item.name.toLowerCase().includes(query.toLowerCase())
|
||||
})
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
disabled={disabled}
|
||||
value={selectedItem}
|
||||
className={className}
|
||||
onChange={(value: Item) => {
|
||||
if (!disabled) {
|
||||
setSelectedItem(value)
|
||||
setOpen(false)
|
||||
onSelect(value)
|
||||
}
|
||||
}}>
|
||||
<div className={classNames('relative')}>
|
||||
<div className='group text-gray-800'>
|
||||
{allowSearch
|
||||
? <Combobox.Input
|
||||
className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-not-allowed`}
|
||||
onChange={(event) => {
|
||||
if (!disabled)
|
||||
setQuery(event.target.value)
|
||||
}}
|
||||
displayValue={(item: Item) => item?.name}
|
||||
/>
|
||||
: <Combobox.Button onClick={
|
||||
() => {
|
||||
if (!disabled)
|
||||
setOpen(!open)
|
||||
}
|
||||
} className={`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`}>
|
||||
{selectedItem?.name}
|
||||
</Combobox.Button>}
|
||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none group-hover:bg-gray-200" onClick={
|
||||
() => {
|
||||
if (!disabled)
|
||||
setOpen(!open)
|
||||
}
|
||||
}>
|
||||
{open ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />}
|
||||
</Combobox.Button>
|
||||
</div>
|
||||
|
||||
{filteredItems.length > 0 && (
|
||||
<Combobox.Options className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
|
||||
{filteredItems.map((item: Item) => (
|
||||
<Combobox.Option
|
||||
key={item.value}
|
||||
value={item}
|
||||
className={({ active }: { active: boolean }) =>
|
||||
classNames(
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700',
|
||||
active ? 'bg-gray-100' : '',
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ /* active, */ selected }) => (
|
||||
<>
|
||||
<span className={classNames('block truncate', selected && 'font-normal')}>{item.name}</span>
|
||||
{selected && (
|
||||
<span
|
||||
className={classNames(
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</div>
|
||||
</Combobox >
|
||||
)
|
||||
}
|
||||
|
||||
const SimpleSelect: FC<ISelectProps> = ({
|
||||
className,
|
||||
items = defaultItems,
|
||||
defaultValue = 1,
|
||||
disabled = false,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null)
|
||||
useEffect(() => {
|
||||
let defaultSelect = null
|
||||
const existed = items.find((item: Item) => item.value === defaultValue)
|
||||
if (existed)
|
||||
defaultSelect = existed
|
||||
|
||||
setSelectedItem(defaultSelect)
|
||||
}, [defaultValue])
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
value={selectedItem}
|
||||
onChange={(value: Item) => {
|
||||
if (!disabled) {
|
||||
setSelectedItem(value)
|
||||
onSelect(value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative h-9">
|
||||
<Listbox.Button className={`w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer ${className}`}>
|
||||
<span className="block truncate text-left">{selectedItem?.name}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
|
||||
{items.map((item: Item) => (
|
||||
<Listbox.Option
|
||||
key={item.value}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
|
||||
}`
|
||||
}
|
||||
value={item}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ /* active, */ selected }) => (
|
||||
<>
|
||||
<span className={classNames('block truncate', selected && 'font-normal')}>{item.name}</span>
|
||||
{selected && (
|
||||
<span
|
||||
className={classNames(
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
)
|
||||
}
|
||||
export { SimpleSelect }
|
||||
export default React.memo(Select)
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
loading?: boolean
|
||||
className?: string
|
||||
children?: React.ReactNode | string
|
||||
}
|
||||
|
||||
const Spinner: FC<Props> = ({ loading = false, children, className }) => {
|
||||
return (
|
||||
<div
|
||||
className={`inline-block text-gray-200 h-4 w-4 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] ${loading ? 'motion-reduce:animate-[spin_1.5s_linear_infinite]' : 'hidden'} ${className ?? ''}`}
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
className="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]"
|
||||
>Loading...</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
@@ -0,0 +1,131 @@
|
||||
'use client'
|
||||
import classNames from 'classnames'
|
||||
import type { ReactNode } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
XCircleIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import { createContext } from 'use-context-selector'
|
||||
|
||||
export type IToastProps = {
|
||||
type?: 'success' | 'error' | 'warning' | 'info'
|
||||
duration?: number
|
||||
message: string
|
||||
children?: ReactNode
|
||||
onClose?: () => void
|
||||
}
|
||||
type IToastContext = {
|
||||
notify: (props: IToastProps) => void
|
||||
}
|
||||
const defaultDuring = 3000
|
||||
|
||||
export const ToastContext = createContext<IToastContext>({} as IToastContext)
|
||||
const Toast = ({
|
||||
type = 'info',
|
||||
duration,
|
||||
message,
|
||||
children,
|
||||
}: IToastProps) => {
|
||||
// sometimes message is react node array. Not handle it.
|
||||
if (typeof message !== 'string')
|
||||
return null
|
||||
|
||||
return <div className={classNames(
|
||||
'fixed rounded-md p-4 my-4 mx-8 z-50',
|
||||
'top-0',
|
||||
'right-0',
|
||||
type === 'success' ? 'bg-green-50' : '',
|
||||
type === 'error' ? 'bg-red-50' : '',
|
||||
type === 'warning' ? 'bg-yellow-50' : '',
|
||||
type === 'info' ? 'bg-blue-50' : '',
|
||||
)}>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
{type === 'success' && <CheckCircleIcon className="w-5 h-5 text-green-400" aria-hidden="true" />}
|
||||
{type === 'error' && <XCircleIcon className="w-5 h-5 text-red-400" aria-hidden="true" />}
|
||||
{type === 'warning' && <ExclamationTriangleIcon className="w-5 h-5 text-yellow-400" aria-hidden="true" />}
|
||||
{type === 'info' && <InformationCircleIcon className="w-5 h-5 text-blue-400" aria-hidden="true" />}
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className={
|
||||
classNames(
|
||||
'text-sm font-medium',
|
||||
type === 'success' ? 'text-green-800' : '',
|
||||
type === 'error' ? 'text-red-800' : '',
|
||||
type === 'warning' ? 'text-yellow-800' : '',
|
||||
type === 'info' ? 'text-blue-800' : '',
|
||||
)
|
||||
}>{message}</h3>
|
||||
{children && <div className={
|
||||
classNames(
|
||||
'mt-2 text-sm',
|
||||
type === 'success' ? 'text-green-700' : '',
|
||||
type === 'error' ? 'text-red-700' : '',
|
||||
type === 'warning' ? 'text-yellow-700' : '',
|
||||
type === 'info' ? 'text-blue-700' : '',
|
||||
)
|
||||
}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export const ToastProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const placeholder: IToastProps = {
|
||||
type: 'info',
|
||||
message: 'Toast message',
|
||||
duration: 3000,
|
||||
}
|
||||
const [params, setParams] = React.useState<IToastProps>(placeholder)
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
setTimeout(() => {
|
||||
setMounted(false)
|
||||
}, params.duration || defaultDuring)
|
||||
}
|
||||
}, [mounted])
|
||||
|
||||
return <ToastContext.Provider value={{
|
||||
notify: (props) => {
|
||||
setMounted(true)
|
||||
setParams(props)
|
||||
},
|
||||
}}>
|
||||
{mounted && <Toast {...params} />}
|
||||
{children}
|
||||
</ToastContext.Provider>
|
||||
}
|
||||
|
||||
Toast.notify = ({
|
||||
type,
|
||||
message,
|
||||
duration,
|
||||
}: Pick<IToastProps, 'type' | 'message' | 'duration'>) => {
|
||||
if (typeof window === 'object') {
|
||||
const holder = document.createElement('div')
|
||||
const root = createRoot(holder)
|
||||
|
||||
root.render(<Toast type={type} message={message} duration={duration} />)
|
||||
document.body.appendChild(holder)
|
||||
setTimeout(() => {
|
||||
if (holder)
|
||||
holder.remove()
|
||||
}, duration || defaultDuring)
|
||||
}
|
||||
}
|
||||
|
||||
export default Toast
|
||||
@@ -0,0 +1,43 @@
|
||||
.toast {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
width: 1.84rem;
|
||||
height: 1.80rem;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
background: #000000;
|
||||
box-shadow: 0 -.04rem .1rem 1px rgba(255, 255, 255, 0.1);
|
||||
border-radius: .1rem .1rem .1rem .1rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: .2rem;
|
||||
height: .4rem;
|
||||
background: center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
/* .success {
|
||||
background-image: url('./icons/success.svg');
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-image: url('./icons/warning.svg');
|
||||
}
|
||||
|
||||
.error {
|
||||
background-image: url('./icons/error.svg');
|
||||
} */
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
font-size: .2rem;
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
import classNames from 'classnames'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { Tooltip as ReactTooltip } from 'react-tooltip' // fixed version to 5.8.3 https://github.com/ReactTooltip/react-tooltip/issues/972
|
||||
import 'react-tooltip/dist/react-tooltip.css'
|
||||
|
||||
type TooltipProps = {
|
||||
selector: string
|
||||
content?: string
|
||||
htmlContent?: React.ReactNode
|
||||
className?: string // This should use !impornant to override the default styles eg: '!bg-white'
|
||||
position?: 'top' | 'right' | 'bottom' | 'left'
|
||||
clickable?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Tooltip: FC<TooltipProps> = ({
|
||||
selector,
|
||||
content,
|
||||
position = 'top',
|
||||
children,
|
||||
htmlContent,
|
||||
className,
|
||||
clickable,
|
||||
}) => {
|
||||
return (
|
||||
<div className='tooltip-container'>
|
||||
{React.cloneElement(children as React.ReactElement, {
|
||||
'data-tooltip-id': selector,
|
||||
})
|
||||
}
|
||||
<ReactTooltip
|
||||
id={selector}
|
||||
content={content}
|
||||
className={classNames('!bg-white !text-xs !font-normal !text-gray-700 !shadow-lg !opacity-100', className)}
|
||||
place={position}
|
||||
clickable={clickable}
|
||||
>
|
||||
{htmlContent && htmlContent}
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tooltip
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PlayIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import Select from '@/app/components/base/select'
|
||||
import type { PromptConfig, AppInfo } from '@/types/app'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
|
||||
export type IConfigSenceProps = {
|
||||
appInfo: AppInfo
|
||||
promptConfig: PromptConfig
|
||||
inputs: Record<string, any>
|
||||
onInputsChange: (inputs: Record<string, any>) => void
|
||||
query: string
|
||||
onQueryChange: (query: string) => void
|
||||
onSend: () => void
|
||||
}
|
||||
const ConfigSence: FC<IConfigSenceProps> = ({
|
||||
appInfo,
|
||||
promptConfig,
|
||||
inputs,
|
||||
onInputsChange,
|
||||
query,
|
||||
onQueryChange,
|
||||
onSend,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="shrink-0 w-1/2 px-10 py-8">
|
||||
<section>
|
||||
{/* title & description */}
|
||||
<div className='text-2xl font-bold text-gray-800'>👏 {t('app.common.welcome')} {appInfo.title}</div>
|
||||
<div className='mt-2 text-gray-400 font-semi-bold'>{appInfo.description}</div>
|
||||
</section>
|
||||
<section>
|
||||
{/* input form */}
|
||||
<form>
|
||||
{promptConfig.prompt_variables.map(item => (
|
||||
<div className='w-full mt-4 inline-flex' key={item.key}>
|
||||
<label className='shrink-0 mr-2 mt-2 text-gray-900 text-sm font-normal w-[120px] leading-4'>{item.name}</label>
|
||||
{item.type === 'select' ? (
|
||||
<Select
|
||||
className='w-full'
|
||||
defaultValue={inputs[item.key]}
|
||||
onSelect={(i) => { onInputsChange({ ...inputs, [item.key]: i.value }) }}
|
||||
items={(item.options || []).map(i => ({ name: i, value: i }))}
|
||||
allowSearch={false}
|
||||
bgClassName='bg-gray-50'
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
|
||||
value={inputs[item.key]}
|
||||
onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
|
||||
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className='w-full mt-4 inline-flex '>
|
||||
<label className='shrink-0 mr-2 mt-2 text-sm text-gray-900 font-normal w-[120px] leading-4'>{t('app.generation.queryTitle')}</label>
|
||||
<div className="w-full mb-4 overflow-hidden border border-gray-200 rounded-lg bg-gray-50 ">
|
||||
<div className="px-4 py-2 bg-gray-50 rounded-t-lg">
|
||||
<textarea
|
||||
value={query}
|
||||
onChange={(e) => { onQueryChange(e.target.value) }}
|
||||
rows={4}
|
||||
className="w-full px-0 text-sm text-gray-900 bg-gray-50 border-0 focus:outline-none placeholder:bg-gray-50" placeholder={t('app.generation.queryPlaceholder') as string}
|
||||
required
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-3 py-2 border-t bg-gray-50">
|
||||
<div className="flex pl-0 space-x-1 sm:pl-2">
|
||||
<span className="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded cursor-pointer">{query.length}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
className='text-primary-600'
|
||||
onClick={onSend}
|
||||
>
|
||||
<PlayIcon className="w-4 h-4 mr-1" aria-hidden="true" />
|
||||
{t('app.generation.run')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigSence)
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
Bars3Icon,
|
||||
PencilSquareIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
export type IHeaderProps = {
|
||||
title: string
|
||||
isMobile?: boolean
|
||||
onShowSideBar?: () => void
|
||||
onCreateNewChat?: () => void
|
||||
}
|
||||
const Header: FC<IHeaderProps> = ({
|
||||
title,
|
||||
isMobile,
|
||||
onShowSideBar,
|
||||
onCreateNewChat,
|
||||
}) => {
|
||||
return (
|
||||
<div className="shrink-0 flex items-center justify-between h-12 px-3 bg-gray-100">
|
||||
{isMobile
|
||||
? (
|
||||
<div
|
||||
className='flex items-center justify-center h-8 w-8 cursor-pointer'
|
||||
onClick={() => onShowSideBar?.()}
|
||||
>
|
||||
<Bars3Icon className="h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
)
|
||||
: <div></div>}
|
||||
<div className='flex items-center space-x-2'>
|
||||
<AppIcon size="small" />
|
||||
<div className=" text-sm text-gray-800 font-bold">{title}</div>
|
||||
</div>
|
||||
{isMobile
|
||||
? (
|
||||
<div className='flex items-center justify-center h-8 w-8 cursor-pointer'
|
||||
onClick={() => onCreateNewChat?.()}
|
||||
>
|
||||
<PencilSquareIcon className="h-4 w-4 text-gray-500" />
|
||||
</div>)
|
||||
: <div></div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
@@ -0,0 +1,178 @@
|
||||
'use client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Header from './header'
|
||||
import ConfigScence from './config-scence'
|
||||
import NoData from './no-data'
|
||||
import Result from './result'
|
||||
import { fetchAppParams, sendCompletionMessage, updateFeedback } from '@/service'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { Feedbacktype, PromptConfig } from '@/types/app'
|
||||
import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import AppUnavailable from '@/app/components/app-unavailable'
|
||||
import { APP_ID, API_KEY, APP_INFO } from '@/config'
|
||||
|
||||
const TextGeneration = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
/*
|
||||
* app info
|
||||
*/
|
||||
const hasSetAppConfig = APP_ID && API_KEY
|
||||
const [appUnavailable, setAppUnavailable] = useState<boolean>(false)
|
||||
const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false)
|
||||
|
||||
const [inputs, setInputs] = useState<Record<string, any>>({})
|
||||
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
|
||||
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [completionRes, setCompletionRes] = useState('')
|
||||
const { notify } = Toast
|
||||
const isNoData = !completionRes
|
||||
const [messageId, setMessageId] = useState<string | null>(null)
|
||||
const [feedback, setFeedback] = useState<Feedbacktype>({
|
||||
rating: null
|
||||
})
|
||||
|
||||
const handleFeedback = async (feedback: Feedbacktype) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } })
|
||||
setFeedback(feedback)
|
||||
}
|
||||
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message })
|
||||
}
|
||||
|
||||
const checkCanSend = () => {
|
||||
const inputLens = Object.values(inputs).length
|
||||
const promptVariablesLens = promptConfig?.prompt_variables.length || 0
|
||||
|
||||
const emytyInput = inputLens < promptVariablesLens || Object.values(inputs).find(v => !v)
|
||||
if (emytyInput) {
|
||||
logError(t('app.errorMessage.valueOfVarRequired'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('app.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!checkCanSend())
|
||||
return
|
||||
|
||||
if (!query) {
|
||||
logError(t('app.errorMessage.queryRequired'))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
const data = {
|
||||
inputs,
|
||||
query,
|
||||
}
|
||||
|
||||
setMessageId(null)
|
||||
setFeedback({
|
||||
rating: null
|
||||
})
|
||||
setCompletionRes('')
|
||||
|
||||
const res: string[] = []
|
||||
let tempMessageId = ''
|
||||
|
||||
setResponsingTrue()
|
||||
sendCompletionMessage(data, {
|
||||
onData: (data: string, _isFirstMessage: boolean, { messageId }: any) => {
|
||||
tempMessageId = messageId
|
||||
res.push(data)
|
||||
setCompletionRes(res.join(''))
|
||||
},
|
||||
onCompleted: () => {
|
||||
setResponsingFalse()
|
||||
setMessageId(tempMessageId)
|
||||
},
|
||||
onError() {
|
||||
setResponsingFalse()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSetAppConfig) {
|
||||
setAppUnavailable(true)
|
||||
return
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
|
||||
changeLanguage(APP_INFO.default_language)
|
||||
|
||||
const { variables: prompt_variables }: any = await fetchAppParams()
|
||||
|
||||
setPromptConfig({
|
||||
prompt_template: '',
|
||||
prompt_variables,
|
||||
} as PromptConfig)
|
||||
}
|
||||
catch (e: any) {
|
||||
if (e.status === 404) {
|
||||
setAppUnavailable(true)
|
||||
}
|
||||
else {
|
||||
setIsUnknwonReason(true)
|
||||
setAppUnavailable(true)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (APP_INFO?.title)
|
||||
document.title = `${APP_INFO.title} - Powered by LangGenius`
|
||||
}, [APP_INFO?.title])
|
||||
|
||||
if (appUnavailable)
|
||||
return <AppUnavailable isUnknwonReason={isUnknwonReason} errMessage={!hasSetAppConfig ? 'Please set APP_ID and API_KEY in config/index.tsx' : ''} />
|
||||
|
||||
if (!APP_INFO || !promptConfig)
|
||||
return <Loading type='app' />
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col h-screen'>
|
||||
<Header title={APP_INFO.title} />
|
||||
<div className="flex grow">
|
||||
<ConfigScence
|
||||
appInfo={APP_INFO}
|
||||
inputs={inputs}
|
||||
onInputsChange={setInputs}
|
||||
promptConfig={promptConfig}
|
||||
query={query}
|
||||
onQueryChange={setQuery}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
|
||||
<div className="flex w-1/2 h-full px-10 py-8 shrink-0">
|
||||
{isNoData
|
||||
? <NoData />
|
||||
: (
|
||||
<div className='flex flex-col w-full h-full'>
|
||||
<Result showFeedback={!!messageId} content={completionRes} feedback={feedback} onFeedback={handleFeedback} />
|
||||
{/* <History dictionary={dictionary} /> */}
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextGeneration
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
DocumentTextIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
|
||||
export type INoDataProps = {}
|
||||
const NoData: FC<INoDataProps> = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex flex-col h-full w-full justify-center items-center'>
|
||||
<DocumentTextIcon className='text-gray-300 w-12 h-12 mb-3' />
|
||||
<div
|
||||
className='text-gray-300 text-xs leading-3'
|
||||
>
|
||||
{t('app.generation.noData')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoData)
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ClipboardDocumentIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import type { Feedbacktype } from '@/types/app'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
// import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
|
||||
import copy from 'copy-to-clipboard'
|
||||
type IResultHeaderProps = {
|
||||
result: string
|
||||
showFeedback: boolean
|
||||
feedback: Feedbacktype
|
||||
onFeedback: (feedback: Feedbacktype) => void
|
||||
}
|
||||
|
||||
const Header: FC<IResultHeaderProps> = ({
|
||||
feedback,
|
||||
showFeedback,
|
||||
onFeedback,
|
||||
result,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex w-full justify-between items-center '>
|
||||
<div className='text-gray-800 text-2xl leading-4 font-normal'>{t('app.generation.resultTitle')}</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button
|
||||
className='flex items-center !h-7 !p-[2px] !pr-2'
|
||||
onClick={() => {
|
||||
copy(result)
|
||||
Toast.notify({ type: 'success', message: 'copied' })
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<ClipboardDocumentIcon className='text-gray-500 w-4 h-3 mr-1' />
|
||||
<span className='text-gray-500 text-xs leading-3'>{t('app.generation.copy')}</span>
|
||||
</>
|
||||
</Button>
|
||||
|
||||
{showFeedback && feedback.rating && feedback.rating === 'like' && (
|
||||
<Tooltip
|
||||
selector="undo-feedback-like"
|
||||
content="Undo Great Rating"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: null
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showFeedback && feedback.rating && feedback.rating === 'dislike' && (
|
||||
<Tooltip
|
||||
selector="undo-feedback-dislike"
|
||||
content="Undo Undesirable Response"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: null
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showFeedback && !feedback.rating && (
|
||||
<div className='flex rounded-lg border border-gray-200 p-[1px] space-x-1'>
|
||||
<Tooltip
|
||||
selector="feedback-like"
|
||||
content="Great Rating"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: 'like'
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
selector="feedback-dislike"
|
||||
content="Undesirable Response"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: 'dislike'
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Header from './header'
|
||||
import type { Feedbacktype } from '@/types/app'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
|
||||
export type IResultProps = {
|
||||
content: string
|
||||
showFeedback: boolean
|
||||
feedback: Feedbacktype
|
||||
onFeedback: (feedback: Feedbacktype) => void
|
||||
}
|
||||
const Result: FC<IResultProps> = ({
|
||||
content,
|
||||
showFeedback,
|
||||
feedback,
|
||||
onFeedback
|
||||
}) => {
|
||||
return (
|
||||
<div className='basis-3/4 h-max'>
|
||||
<Header result={content} showFeedback={showFeedback} feedback={feedback} onFeedback={onFeedback} />
|
||||
<div
|
||||
className='mt-4 w-full flex text-sm leading-5 overflow-scroll font-normal text-gray-900'
|
||||
style={{
|
||||
maxHeight: '70vh'
|
||||
}}
|
||||
>
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Result)
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare module 'langgenius-client';
|
||||
declare module 'uuid';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
|
||||
import './styles/globals.css'
|
||||
import './styles/markdown.scss'
|
||||
|
||||
const LocaleLayout = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const locale = getLocaleOnServer()
|
||||
return (
|
||||
<html lang={locale ?? 'en'} className="h-full">
|
||||
<body className="h-full">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="w-screen h-screen min-w-[300px]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export default LocaleLayout
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
import Main from '@/app/components'
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Main />
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(App)
|
||||
@@ -0,0 +1,128 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
||||
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
||||
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
--primary-glow: conic-gradient(from 180deg at 50% 50%,
|
||||
#16abff33 0deg,
|
||||
#0885ff33 55deg,
|
||||
#54d6ff33 120deg,
|
||||
#0071ff33 160deg,
|
||||
transparent 360deg);
|
||||
--secondary-glow: radial-gradient(rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0));
|
||||
|
||||
--tile-start-rgb: 239, 245, 249;
|
||||
--tile-end-rgb: 228, 232, 233;
|
||||
--tile-border: conic-gradient(#00000080,
|
||||
#00000040,
|
||||
#00000030,
|
||||
#00000020,
|
||||
#00000010,
|
||||
#00000010,
|
||||
#00000080);
|
||||
|
||||
--callout-rgb: 238, 240, 241;
|
||||
--callout-border-rgb: 172, 175, 176;
|
||||
--card-rgb: 180, 185, 188;
|
||||
--card-border-rgb: 131, 134, 135;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
|
||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
||||
--secondary-glow: linear-gradient(to bottom right,
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0.3));
|
||||
|
||||
--tile-start-rgb: 2, 13, 46;
|
||||
--tile-end-rgb: 2, 5, 19;
|
||||
--tile-border: conic-gradient(#ffffff80,
|
||||
#ffffff40,
|
||||
#ffffff30,
|
||||
#ffffff20,
|
||||
#ffffff10,
|
||||
#ffffff10,
|
||||
#ffffff80);
|
||||
|
||||
--callout-rgb: 20, 20, 20;
|
||||
--callout-border-rgb: 108, 108, 108;
|
||||
--card-rgb: 100, 100, 100;
|
||||
--card-border-rgb: 200, 200, 200;
|
||||
}
|
||||
} */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
/* background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb)); */
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
} */
|
||||
|
||||
/* CSS Utils */
|
||||
.h1 {
|
||||
padding-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
font-size: 1.125rem;
|
||||
color: #111928;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111928;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply text-blue-600 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(91.58deg, #2250F2 -29.55%, #0EBCF3 75.22%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
import { AppInfo } from "@/types/app"
|
||||
export const APP_ID = ''
|
||||
export const API_KEY = ''
|
||||
|
||||
export const APP_INFO: AppInfo = {
|
||||
"title": 'Text Generator APP',
|
||||
"description": 'App description',
|
||||
"copyright": '',
|
||||
"privacy_policy": '',
|
||||
"default_language": 'zh-Hans'
|
||||
}
|
||||
|
||||
export const API_PREFIX = '/api';
|
||||
|
||||
export const LOCALE_COOKIE_NAME = 'locale'
|
||||
|
||||
export const DEFAULT_VALUE_MAX_LEN = 48
|
||||
@@ -0,0 +1,18 @@
|
||||
import Cookies from 'js-cookie'
|
||||
import type { Locale } from '.'
|
||||
import { i18n } from '.'
|
||||
import { LOCALE_COOKIE_NAME } from '@/config'
|
||||
import { changeLanguage } from '@/i18n/i18next-config'
|
||||
|
||||
// same logic as server
|
||||
export const getLocaleOnClient = (): Locale => {
|
||||
return Cookies.get(LOCALE_COOKIE_NAME) as Locale || i18n.defaultLocale
|
||||
}
|
||||
|
||||
export const setLocaleOnClient = (locale: Locale, notReload?: boolean) => {
|
||||
Cookies.set(LOCALE_COOKIE_NAME, locale)
|
||||
changeLanguage(locale)
|
||||
if (!notReload) {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import commonEn from './lang/common.en'
|
||||
import commonZh from './lang/common.zh'
|
||||
import appEn from './lang/app.en'
|
||||
import appZh from './lang/app.zh'
|
||||
import { Locale } from '.'
|
||||
|
||||
const resources = {
|
||||
'en': {
|
||||
translation: {
|
||||
common: commonEn,
|
||||
app: appEn,
|
||||
},
|
||||
},
|
||||
'zh-Hans': {
|
||||
translation: {
|
||||
common: commonZh,
|
||||
app: appZh,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
i18n.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
// debug: true,
|
||||
resources,
|
||||
})
|
||||
|
||||
export const changeLanguage = (lan: Locale) => {
|
||||
i18n.changeLanguage(lan)
|
||||
}
|
||||
export default i18n
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createInstance } from 'i18next'
|
||||
import resourcesToBackend from 'i18next-resources-to-backend'
|
||||
import { initReactI18next } from 'react-i18next/initReactI18next'
|
||||
import { Locale } from '.'
|
||||
|
||||
// https://locize.com/blog/next-13-app-dir-i18n/
|
||||
const initI18next = async (lng: Locale, ns: string) => {
|
||||
const i18nInstance = createInstance()
|
||||
await i18nInstance
|
||||
.use(initReactI18next)
|
||||
.use(resourcesToBackend((language: string, namespace: string) => import(`./lang/${namespace}.${language}.ts`)))
|
||||
.init({
|
||||
lng: lng === 'zh-Hans' ? 'zh' : lng,
|
||||
ns,
|
||||
fallbackLng: 'en',
|
||||
})
|
||||
return i18nInstance
|
||||
}
|
||||
|
||||
export async function useTranslation(lng: Locale, ns = '', options: Record<string, any> = {}) {
|
||||
const i18nextInstance = await initI18next(lng, ns)
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix),
|
||||
i18n: i18nextInstance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const i18n = {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'zh-Hans'],
|
||||
} as const
|
||||
|
||||
export type Locale = typeof i18n['locales'][number]
|
||||
@@ -0,0 +1,22 @@
|
||||
const translation = {
|
||||
common: {
|
||||
welcome: "Welcome to use",
|
||||
appUnavailable: "App is unavailable",
|
||||
appUnkonwError: "App is unavailable"
|
||||
},
|
||||
generation: {
|
||||
queryTitle: "Query content",
|
||||
queryPlaceholder: "Write your query content...",
|
||||
run: "RUN",
|
||||
copy: "Copy",
|
||||
resultTitle: "AI Completion",
|
||||
noData: "AI will give you what you want here.",
|
||||
},
|
||||
errorMessage: {
|
||||
valueOfVarRequired: "Variables value can not be empty",
|
||||
queryRequired: "Request text is required.",
|
||||
waitForResponse: "Please wait for the response to the previous message to complete.",
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
||||
@@ -0,0 +1,22 @@
|
||||
const translation = {
|
||||
common: {
|
||||
welcome: "欢迎使用",
|
||||
appUnavailable: "应用不可用",
|
||||
appUnkonwError: "应用不可用",
|
||||
},
|
||||
generation: {
|
||||
queryTitle: "查询内容",
|
||||
queryPlaceholder: "请输入文本内容",
|
||||
run: "运行",
|
||||
copy: "拷贝",
|
||||
resultTitle: "AI 智能书写",
|
||||
noData: "AI 会在这里给你惊喜。",
|
||||
},
|
||||
errorMessage: {
|
||||
valueOfVarRequired: "变量值必填",
|
||||
queryRequired: "主要文本必填",
|
||||
waitForResponse: "请等待上条信息响应完成",
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
||||
@@ -0,0 +1,22 @@
|
||||
const translation = {
|
||||
api: {
|
||||
success: 'Success',
|
||||
saved: 'Saved',
|
||||
create: 'Created',
|
||||
},
|
||||
operation: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
clear: 'Clear',
|
||||
save: 'Save',
|
||||
edit: 'Edit',
|
||||
refresh: 'Restart',
|
||||
search: 'Search',
|
||||
send: 'Send',
|
||||
lineBreak: 'Line break',
|
||||
like: 'like',
|
||||
dislike: 'dislike',
|
||||
}
|
||||
}
|
||||
|
||||
export default translation
|
||||
@@ -0,0 +1,22 @@
|
||||
const translation = {
|
||||
api: {
|
||||
success: '成功',
|
||||
saved: '已保存',
|
||||
create: '已创建',
|
||||
},
|
||||
operation: {
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
clear: '清空',
|
||||
save: '保存',
|
||||
edit: '编辑',
|
||||
refresh: '重新开始',
|
||||
search: '搜索',
|
||||
send: '发送',
|
||||
lineBreak: '换行',
|
||||
like: '赞同',
|
||||
dislike: '反对',
|
||||
}
|
||||
}
|
||||
|
||||
export default translation
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'server-only'
|
||||
|
||||
import { cookies, headers } from 'next/headers'
|
||||
import Negotiator from 'negotiator'
|
||||
import { match } from '@formatjs/intl-localematcher'
|
||||
import type { Locale } from '.'
|
||||
import { i18n } from '.'
|
||||
|
||||
export const getLocaleOnServer = (): Locale => {
|
||||
// @ts-expect-error locales are readonly
|
||||
const locales: string[] = i18n.locales
|
||||
|
||||
let languages: string[] | undefined
|
||||
// get locale from cookie
|
||||
const localeCookie = 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> = {}
|
||||
headers().forEach((value, key) => (negotiatorHeaders[key] = value))
|
||||
// Use negotiator and intl-localematcher to get best locale
|
||||
languages = new Negotiator({ headers: negotiatorHeaders }).languages()
|
||||
}
|
||||
|
||||
// match locale
|
||||
const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
|
||||
return matchedLocale
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
productionBrowserSourceMaps: false, // enable browser source map generation during the production build
|
||||
// Configure pageExtensions to include md and mdx
|
||||
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
// fix all before production. Now it slow the develop speed.
|
||||
eslint: {
|
||||
// Warning: This allows production builds to successfully complete even if
|
||||
// your project has ESLint errors.
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
|
||||
ignoreBuildErrors: true,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "webapp-text-generator",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"fix": "next lint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.2.32",
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@heroicons/react": "^2.0.16",
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"ahooks": "^3.7.5",
|
||||
"axios": "^1.3.5",
|
||||
"classnames": "^2.3.2",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"eventsource-parser": "^1.0.0",
|
||||
"i18next": "^22.4.13",
|
||||
"i18next-resources-to-backend": "^1.1.3",
|
||||
"immer": "^9.0.19",
|
||||
"js-cookie": "^3.0.1",
|
||||
"langgenius-client": "^1.0.1",
|
||||
"negotiator": "^0.6.3",
|
||||
"next": "13.2.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^4.0.2",
|
||||
"react-headless-pagination": "^1.1.4",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tooltip": "5.8.3",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"sass": "^1.61.0",
|
||||
"scheduler": "^0.23.0",
|
||||
"server-only": "^0.0.1",
|
||||
"swr": "^2.1.0",
|
||||
"typescript": "4.9.5",
|
||||
"use-context-selector": "^1.4.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.36.0",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/negotiator": "^0.6.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"miragejs": "^0.1.47",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.2.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
+224
@@ -0,0 +1,224 @@
|
||||
import { API_PREFIX } from '@/config'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const TIME_OUT = 100000
|
||||
|
||||
const ContentType = {
|
||||
json: 'application/json',
|
||||
stream: 'text/event-stream',
|
||||
form: 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
download: 'application/octet-stream', // for download
|
||||
}
|
||||
|
||||
const baseOptions = {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'include', // always send cookies、HTTP Basic authentication.
|
||||
headers: new Headers({
|
||||
'Content-Type': ContentType.json,
|
||||
}),
|
||||
redirect: 'follow',
|
||||
}
|
||||
|
||||
export type IOnDataMoreInfo = {
|
||||
conversationId: string | undefined
|
||||
messageId: string
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
|
||||
export type IOnCompleted = () => void
|
||||
export type IOnError = (msg: string) => void
|
||||
|
||||
type IOtherOptions = {
|
||||
needAllResponseContent?: boolean
|
||||
onData?: IOnData // for stream
|
||||
onError?: IOnError
|
||||
onCompleted?: IOnCompleted // for stream
|
||||
}
|
||||
|
||||
function unicodeToChar(text: string) {
|
||||
return text.replace(/\\u[0-9a-f]{4}/g, (_match, p1) => {
|
||||
return String.fromCharCode(parseInt(p1, 16))
|
||||
})
|
||||
}
|
||||
|
||||
const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted) => {
|
||||
if (!response.ok)
|
||||
throw new Error('Network response was not ok')
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let buffer = ''
|
||||
let bufferObj: any
|
||||
let isFirstMessage = true
|
||||
function read() {
|
||||
reader.read().then((result: any) => {
|
||||
if (result.done) {
|
||||
onCompleted && onCompleted()
|
||||
return
|
||||
}
|
||||
buffer += decoder.decode(result.value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
try {
|
||||
lines.forEach((message) => {
|
||||
if (!message) return
|
||||
bufferObj = JSON.parse(message) // remove data: and parse as json
|
||||
onData(unicodeToChar(bufferObj.answer), isFirstMessage, {
|
||||
conversationId: bufferObj.conversation_id,
|
||||
messageId: bufferObj.id,
|
||||
})
|
||||
isFirstMessage = false
|
||||
})
|
||||
buffer = lines[lines.length - 1]
|
||||
} catch (e) {
|
||||
onData('', false, {
|
||||
conversationId: undefined,
|
||||
messageId: '',
|
||||
errorMessage: e + ''
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
read()
|
||||
})
|
||||
}
|
||||
read()
|
||||
}
|
||||
|
||||
const baseFetch = (url: string, fetchOptions: any, { needAllResponseContent }: IOtherOptions) => {
|
||||
const options = Object.assign({}, baseOptions, fetchOptions)
|
||||
|
||||
let urlPrefix = API_PREFIX
|
||||
|
||||
let urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`
|
||||
|
||||
const { method, params, body } = options
|
||||
// handle query
|
||||
if (method === 'GET' && params) {
|
||||
const paramsArray: string[] = []
|
||||
Object.keys(params).forEach(key =>
|
||||
paramsArray.push(`${key}=${encodeURIComponent(params[key])}`),
|
||||
)
|
||||
if (urlWithPrefix.search(/\?/) === -1)
|
||||
urlWithPrefix += `?${paramsArray.join('&')}`
|
||||
|
||||
else
|
||||
urlWithPrefix += `&${paramsArray.join('&')}`
|
||||
|
||||
delete options.params
|
||||
}
|
||||
|
||||
if (body)
|
||||
options.body = JSON.stringify(body)
|
||||
|
||||
// Handle timeout
|
||||
return Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('request timeout'))
|
||||
}, TIME_OUT)
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
globalThis.fetch(urlWithPrefix, options)
|
||||
.then((res: any) => {
|
||||
const resClone = res.clone()
|
||||
// Error handler
|
||||
if (!/^(2|3)\d{2}$/.test(res.status)) {
|
||||
const bodyJson = res.json()
|
||||
switch (res.status) {
|
||||
case 401: {
|
||||
Toast.notify({ type: 'error', message: 'Invalid token' })
|
||||
return
|
||||
|
||||
}
|
||||
default:
|
||||
// eslint-disable-next-line no-new
|
||||
new Promise(() => {
|
||||
bodyJson.then((data: any) => {
|
||||
Toast.notify({ type: 'error', message: data.message })
|
||||
})
|
||||
})
|
||||
}
|
||||
return Promise.reject(resClone)
|
||||
}
|
||||
|
||||
// handle delete api. Delete api not return content.
|
||||
if (res.status === 204) {
|
||||
resolve({ result: "success" })
|
||||
return
|
||||
}
|
||||
|
||||
// return data
|
||||
const data = options.headers.get('Content-type') === ContentType.download ? res.blob() : res.json()
|
||||
|
||||
resolve(needAllResponseContent ? resClone : data)
|
||||
})
|
||||
.catch((err) => {
|
||||
Toast.notify({ type: 'error', message: err })
|
||||
reject(err)
|
||||
})
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
export const ssePost = (url: string, fetchOptions: any, {
|
||||
onData, onCompleted, onError }: IOtherOptions) => {
|
||||
const options = Object.assign({}, baseOptions, {
|
||||
method: 'POST',
|
||||
}, fetchOptions)
|
||||
|
||||
const urlPrefix = API_PREFIX
|
||||
const urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`
|
||||
|
||||
const { body } = options
|
||||
if (body)
|
||||
options.body = JSON.stringify(body)
|
||||
|
||||
globalThis.fetch(urlWithPrefix, options)
|
||||
.then((res: any) => {
|
||||
if (!/^(2|3)\d{2}$/.test(res.status)) {
|
||||
// eslint-disable-next-line no-new
|
||||
new Promise(() => {
|
||||
debugger
|
||||
res.json().then((data: any) => {
|
||||
Toast.notify({ type: 'error', message: data.message || 'Server Error' })
|
||||
})
|
||||
})
|
||||
onError?.('Server Error')
|
||||
return
|
||||
}
|
||||
return handleStream(res, (str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => {
|
||||
if (moreInfo.errorMessage) {
|
||||
Toast.notify({ type: 'error', message: moreInfo.errorMessage })
|
||||
return
|
||||
}
|
||||
onData?.(str, isFirstMessage, moreInfo)
|
||||
}, () => {
|
||||
onCompleted?.()
|
||||
})
|
||||
}).catch((e) => {
|
||||
Toast.notify({ type: 'error', message: e })
|
||||
onError?.(e)
|
||||
})
|
||||
}
|
||||
|
||||
export const request = (url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return baseFetch(url, options, otherOptions || {})
|
||||
}
|
||||
|
||||
export const get = (url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return request(url, Object.assign({}, options, { method: 'GET' }), otherOptions)
|
||||
}
|
||||
|
||||
export const post = (url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return request(url, Object.assign({}, options, { method: 'POST' }), otherOptions)
|
||||
}
|
||||
|
||||
export const put = (url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return request(url, Object.assign({}, options, { method: 'PUT' }), otherOptions)
|
||||
}
|
||||
|
||||
export const del = (url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return request(url, Object.assign({}, options, { method: 'DELETE' }), otherOptions)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { IOnCompleted, IOnData, IOnError } from './base'
|
||||
import { get, post, ssePost } from './base'
|
||||
import type { Feedbacktype } from '@/types/app'
|
||||
|
||||
export const sendCompletionMessage = async (body: Record<string, any>, { onData, onCompleted, onError }: {
|
||||
onData: IOnData
|
||||
onCompleted: IOnCompleted
|
||||
onError: IOnError
|
||||
}) => {
|
||||
return ssePost('completion-messages', {
|
||||
body: {
|
||||
...body,
|
||||
response_mode: 'streaming',
|
||||
},
|
||||
}, { onData, onCompleted, onError })
|
||||
}
|
||||
|
||||
export const fetchAppParams = async () => {
|
||||
return get('parameters')
|
||||
}
|
||||
|
||||
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
|
||||
return post(url, { body })
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./app/**/*.{js,ts,jsx,tsx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
typography: require('./typography'),
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
50: '#F9FAFB',
|
||||
100: '#F3F4F6',
|
||||
200: '#E5E7EB',
|
||||
300: '#D1D5DB',
|
||||
400: '#9CA3AF',
|
||||
500: '#6B7280',
|
||||
700: '#374151',
|
||||
800: '#1F2A37',
|
||||
900: '#111928',
|
||||
},
|
||||
primary: {
|
||||
50: '#EBF5FF',
|
||||
100: '#E1EFFE',
|
||||
200: '#C3DDFD',
|
||||
300: '#A4CAFE',
|
||||
600: '#1C64F2',
|
||||
700: '#1A56DB',
|
||||
},
|
||||
blue: {
|
||||
500: '#E1EFFE',
|
||||
},
|
||||
green: {
|
||||
50: '#F3FAF7',
|
||||
100: '#DEF7EC',
|
||||
800: '#03543F',
|
||||
|
||||
},
|
||||
yellow: {
|
||||
100: '#FDF6B2',
|
||||
800: '#723B13',
|
||||
},
|
||||
purple: {
|
||||
50: '#F6F5FF',
|
||||
},
|
||||
indigo: {
|
||||
25: '#F5F8FF',
|
||||
100: '#E0EAFF',
|
||||
600: '#444CE7'
|
||||
}
|
||||
},
|
||||
screens: {
|
||||
'mobile': '100px',
|
||||
// => @media (min-width: 100px) { ... }
|
||||
'tablet': '640px', // 391
|
||||
// => @media (min-width: 600px) { ... }
|
||||
'pc': '769px',
|
||||
// => @media (min-width: 769px) { ... }
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/line-clamp'),
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"global.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"app/components/develop/Prose.jsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Locale } from '@/i18n'
|
||||
|
||||
export type AppInfo = {
|
||||
title: string
|
||||
description: string
|
||||
default_language: Locale
|
||||
copyright?: string
|
||||
privacy_policy?: string
|
||||
}
|
||||
|
||||
export type PromptVariable = {
|
||||
key: string,
|
||||
name: string,
|
||||
type: "string" | "number" | "select",
|
||||
default?: string | number,
|
||||
options?: string[]
|
||||
max_length: number
|
||||
}
|
||||
|
||||
export type PromptConfig = {
|
||||
prompt_template: string,
|
||||
prompt_variables: PromptVariable[],
|
||||
}
|
||||
|
||||
export const MessageRatings = ['like', 'dislike', null] as const
|
||||
export type MessageRating = typeof MessageRatings[number]
|
||||
|
||||
export type Feedbacktype = {
|
||||
rating: MessageRating
|
||||
content?: string | null
|
||||
}
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
module.exports = ({ theme }) => ({
|
||||
DEFAULT: {
|
||||
css: {
|
||||
'--tw-prose-body': theme('colors.zinc.700'),
|
||||
'--tw-prose-headings': theme('colors.zinc.900'),
|
||||
'--tw-prose-links': theme('colors.emerald.500'),
|
||||
'--tw-prose-links-hover': theme('colors.emerald.600'),
|
||||
'--tw-prose-links-underline': theme('colors.emerald.500 / 0.3'),
|
||||
'--tw-prose-bold': theme('colors.zinc.900'),
|
||||
'--tw-prose-counters': theme('colors.zinc.500'),
|
||||
'--tw-prose-bullets': theme('colors.zinc.300'),
|
||||
'--tw-prose-hr': theme('colors.zinc.900 / 0.05'),
|
||||
'--tw-prose-quotes': theme('colors.zinc.900'),
|
||||
'--tw-prose-quote-borders': theme('colors.zinc.200'),
|
||||
'--tw-prose-captions': theme('colors.zinc.500'),
|
||||
'--tw-prose-code': theme('colors.zinc.900'),
|
||||
'--tw-prose-code-bg': theme('colors.zinc.100'),
|
||||
'--tw-prose-code-ring': theme('colors.zinc.300'),
|
||||
'--tw-prose-th-borders': theme('colors.zinc.300'),
|
||||
'--tw-prose-td-borders': theme('colors.zinc.200'),
|
||||
|
||||
'--tw-prose-invert-body': theme('colors.zinc.400'),
|
||||
'--tw-prose-invert-headings': theme('colors.white'),
|
||||
'--tw-prose-invert-links': theme('colors.emerald.400'),
|
||||
'--tw-prose-invert-links-hover': theme('colors.emerald.500'),
|
||||
'--tw-prose-invert-links-underline': theme('colors.emerald.500 / 0.3'),
|
||||
'--tw-prose-invert-bold': theme('colors.white'),
|
||||
'--tw-prose-invert-counters': theme('colors.zinc.400'),
|
||||
'--tw-prose-invert-bullets': theme('colors.zinc.600'),
|
||||
'--tw-prose-invert-hr': theme('colors.white / 0.05'),
|
||||
'--tw-prose-invert-quotes': theme('colors.zinc.100'),
|
||||
'--tw-prose-invert-quote-borders': theme('colors.zinc.700'),
|
||||
'--tw-prose-invert-captions': theme('colors.zinc.400'),
|
||||
'--tw-prose-invert-code': theme('colors.white'),
|
||||
'--tw-prose-invert-code-bg': theme('colors.zinc.700 / 0.15'),
|
||||
'--tw-prose-invert-code-ring': theme('colors.white / 0.1'),
|
||||
'--tw-prose-invert-th-borders': theme('colors.zinc.600'),
|
||||
'--tw-prose-invert-td-borders': theme('colors.zinc.700'),
|
||||
|
||||
// Base
|
||||
color: 'var(--tw-prose-body)',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
lineHeight: theme('lineHeight.7'),
|
||||
|
||||
// Layout
|
||||
'> *': {
|
||||
maxWidth: theme('maxWidth.2xl'),
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
'@screen lg': {
|
||||
maxWidth: theme('maxWidth.3xl'),
|
||||
marginLeft: `calc(50% - min(50%, ${theme('maxWidth.lg')}))`,
|
||||
marginRight: `calc(50% - min(50%, ${theme('maxWidth.lg')}))`,
|
||||
},
|
||||
},
|
||||
|
||||
// Text
|
||||
p: {
|
||||
marginTop: theme('spacing.6'),
|
||||
marginBottom: theme('spacing.6'),
|
||||
},
|
||||
'[class~="lead"]': {
|
||||
fontSize: theme('fontSize.base')[0],
|
||||
...theme('fontSize.base')[1],
|
||||
},
|
||||
|
||||
// Lists
|
||||
ol: {
|
||||
listStyleType: 'decimal',
|
||||
marginTop: theme('spacing.5'),
|
||||
marginBottom: theme('spacing.5'),
|
||||
paddingLeft: '1.625rem',
|
||||
},
|
||||
'ol[type="A"]': {
|
||||
listStyleType: 'upper-alpha',
|
||||
},
|
||||
'ol[type="a"]': {
|
||||
listStyleType: 'lower-alpha',
|
||||
},
|
||||
'ol[type="A" s]': {
|
||||
listStyleType: 'upper-alpha',
|
||||
},
|
||||
'ol[type="a" s]': {
|
||||
listStyleType: 'lower-alpha',
|
||||
},
|
||||
'ol[type="I"]': {
|
||||
listStyleType: 'upper-roman',
|
||||
},
|
||||
'ol[type="i"]': {
|
||||
listStyleType: 'lower-roman',
|
||||
},
|
||||
'ol[type="I" s]': {
|
||||
listStyleType: 'upper-roman',
|
||||
},
|
||||
'ol[type="i" s]': {
|
||||
listStyleType: 'lower-roman',
|
||||
},
|
||||
'ol[type="1"]': {
|
||||
listStyleType: 'decimal',
|
||||
},
|
||||
ul: {
|
||||
listStyleType: 'disc',
|
||||
marginTop: theme('spacing.5'),
|
||||
marginBottom: theme('spacing.5'),
|
||||
paddingLeft: '1.625rem',
|
||||
},
|
||||
li: {
|
||||
marginTop: theme('spacing.2'),
|
||||
marginBottom: theme('spacing.2'),
|
||||
},
|
||||
':is(ol, ul) > li': {
|
||||
paddingLeft: theme('spacing[1.5]'),
|
||||
},
|
||||
'ol > li::marker': {
|
||||
fontWeight: '400',
|
||||
color: 'var(--tw-prose-counters)',
|
||||
},
|
||||
'ul > li::marker': {
|
||||
color: 'var(--tw-prose-bullets)',
|
||||
},
|
||||
'> ul > li p': {
|
||||
marginTop: theme('spacing.3'),
|
||||
marginBottom: theme('spacing.3'),
|
||||
},
|
||||
'> ul > li > *:first-child': {
|
||||
marginTop: theme('spacing.5'),
|
||||
},
|
||||
'> ul > li > *:last-child': {
|
||||
marginBottom: theme('spacing.5'),
|
||||
},
|
||||
'> ol > li > *:first-child': {
|
||||
marginTop: theme('spacing.5'),
|
||||
},
|
||||
'> ol > li > *:last-child': {
|
||||
marginBottom: theme('spacing.5'),
|
||||
},
|
||||
'ul ul, ul ol, ol ul, ol ol': {
|
||||
marginTop: theme('spacing.3'),
|
||||
marginBottom: theme('spacing.3'),
|
||||
},
|
||||
|
||||
// Horizontal rules
|
||||
hr: {
|
||||
borderColor: 'var(--tw-prose-hr)',
|
||||
borderTopWidth: 1,
|
||||
marginTop: theme('spacing.16'),
|
||||
marginBottom: theme('spacing.16'),
|
||||
maxWidth: 'none',
|
||||
marginLeft: `calc(-1 * ${theme('spacing.4')})`,
|
||||
marginRight: `calc(-1 * ${theme('spacing.4')})`,
|
||||
'@screen sm': {
|
||||
marginLeft: `calc(-1 * ${theme('spacing.6')})`,
|
||||
marginRight: `calc(-1 * ${theme('spacing.6')})`,
|
||||
},
|
||||
'@screen lg': {
|
||||
marginLeft: `calc(-1 * ${theme('spacing.8')})`,
|
||||
marginRight: `calc(-1 * ${theme('spacing.8')})`,
|
||||
},
|
||||
},
|
||||
|
||||
// Quotes
|
||||
blockquote: {
|
||||
fontWeight: '500',
|
||||
fontStyle: 'italic',
|
||||
color: 'var(--tw-prose-quotes)',
|
||||
borderLeftWidth: '0.25rem',
|
||||
borderLeftColor: 'var(--tw-prose-quote-borders)',
|
||||
quotes: '"\\201C""\\201D""\\2018""\\2019"',
|
||||
marginTop: theme('spacing.8'),
|
||||
marginBottom: theme('spacing.8'),
|
||||
paddingLeft: theme('spacing.5'),
|
||||
},
|
||||
'blockquote p:first-of-type::before': {
|
||||
content: 'open-quote',
|
||||
},
|
||||
'blockquote p:last-of-type::after': {
|
||||
content: 'close-quote',
|
||||
},
|
||||
|
||||
// Headings
|
||||
h1: {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: '700',
|
||||
fontSize: theme('fontSize.2xl')[0],
|
||||
...theme('fontSize.2xl')[1],
|
||||
marginBottom: theme('spacing.2'),
|
||||
},
|
||||
h2: {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: '600',
|
||||
fontSize: theme('fontSize.lg')[0],
|
||||
...theme('fontSize.lg')[1],
|
||||
marginTop: theme('spacing.16'),
|
||||
marginBottom: theme('spacing.2'),
|
||||
},
|
||||
h3: {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontSize: theme('fontSize.base')[0],
|
||||
...theme('fontSize.base')[1],
|
||||
fontWeight: '600',
|
||||
marginTop: theme('spacing.10'),
|
||||
marginBottom: theme('spacing.2'),
|
||||
},
|
||||
|
||||
// Media
|
||||
'img, video, figure': {
|
||||
marginTop: theme('spacing.8'),
|
||||
marginBottom: theme('spacing.8'),
|
||||
},
|
||||
'figure > *': {
|
||||
marginTop: '0',
|
||||
marginBottom: '0',
|
||||
},
|
||||
figcaption: {
|
||||
color: 'var(--tw-prose-captions)',
|
||||
fontSize: theme('fontSize.xs')[0],
|
||||
...theme('fontSize.xs')[1],
|
||||
marginTop: theme('spacing.2'),
|
||||
},
|
||||
|
||||
// Tables
|
||||
table: {
|
||||
width: '100%',
|
||||
tableLayout: 'auto',
|
||||
textAlign: 'left',
|
||||
marginTop: theme('spacing.8'),
|
||||
marginBottom: theme('spacing.8'),
|
||||
lineHeight: theme('lineHeight.6'),
|
||||
},
|
||||
thead: {
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'var(--tw-prose-th-borders)',
|
||||
},
|
||||
'thead th': {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: '600',
|
||||
verticalAlign: 'bottom',
|
||||
paddingRight: theme('spacing.2'),
|
||||
paddingBottom: theme('spacing.2'),
|
||||
paddingLeft: theme('spacing.2'),
|
||||
},
|
||||
'thead th:first-child': {
|
||||
paddingLeft: '0',
|
||||
},
|
||||
'thead th:last-child': {
|
||||
paddingRight: '0',
|
||||
},
|
||||
'tbody tr': {
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'var(--tw-prose-td-borders)',
|
||||
},
|
||||
'tbody tr:last-child': {
|
||||
borderBottomWidth: '0',
|
||||
},
|
||||
'tbody td': {
|
||||
verticalAlign: 'baseline',
|
||||
},
|
||||
tfoot: {
|
||||
borderTopWidth: '1px',
|
||||
borderTopColor: 'var(--tw-prose-th-borders)',
|
||||
},
|
||||
'tfoot td': {
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
':is(tbody, tfoot) td': {
|
||||
paddingTop: theme('spacing.2'),
|
||||
paddingRight: theme('spacing.2'),
|
||||
paddingBottom: theme('spacing.2'),
|
||||
paddingLeft: theme('spacing.2'),
|
||||
},
|
||||
':is(tbody, tfoot) td:first-child': {
|
||||
paddingLeft: '0',
|
||||
},
|
||||
':is(tbody, tfoot) td:last-child': {
|
||||
paddingRight: '0',
|
||||
},
|
||||
|
||||
// Inline elements
|
||||
a: {
|
||||
color: 'var(--tw-prose-links)',
|
||||
textDecoration: 'underline transparent',
|
||||
fontWeight: '500',
|
||||
transitionProperty: 'color, text-decoration-color',
|
||||
transitionDuration: theme('transitionDuration.DEFAULT'),
|
||||
transitionTimingFunction: theme('transitionTimingFunction.DEFAULT'),
|
||||
'&:hover': {
|
||||
color: 'var(--tw-prose-links-hover)',
|
||||
textDecorationColor: 'var(--tw-prose-links-underline)',
|
||||
},
|
||||
},
|
||||
':is(h1, h2, h3) a': {
|
||||
fontWeight: 'inherit',
|
||||
},
|
||||
strong: {
|
||||
color: 'var(--tw-prose-bold)',
|
||||
fontWeight: '600',
|
||||
},
|
||||
':is(a, blockquote, thead th) strong': {
|
||||
color: 'inherit',
|
||||
},
|
||||
code: {
|
||||
color: 'var(--tw-prose-code)',
|
||||
borderRadius: theme('borderRadius.lg'),
|
||||
paddingTop: theme('padding.1'),
|
||||
paddingRight: theme('padding[1.5]'),
|
||||
paddingBottom: theme('padding.1'),
|
||||
paddingLeft: theme('padding[1.5]'),
|
||||
boxShadow: 'inset 0 0 0 1px var(--tw-prose-code-ring)',
|
||||
backgroundColor: 'var(--tw-prose-code-bg)',
|
||||
fontSize: theme('fontSize.2xs'),
|
||||
},
|
||||
':is(a, h1, h2, h3, blockquote, thead th) code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'h2 code': {
|
||||
fontSize: theme('fontSize.base')[0],
|
||||
fontWeight: 'inherit',
|
||||
},
|
||||
'h3 code': {
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
fontWeight: 'inherit',
|
||||
},
|
||||
|
||||
// Overrides
|
||||
':is(h1, h2, h3) + *': {
|
||||
marginTop: '0',
|
||||
},
|
||||
'> :first-child': {
|
||||
marginTop: '0 !important',
|
||||
},
|
||||
'> :last-child': {
|
||||
marginBottom: '0 !important',
|
||||
},
|
||||
},
|
||||
},
|
||||
invert: {
|
||||
css: {
|
||||
'--tw-prose-body': 'var(--tw-prose-invert-body)',
|
||||
'--tw-prose-headings': 'var(--tw-prose-invert-headings)',
|
||||
'--tw-prose-links': 'var(--tw-prose-invert-links)',
|
||||
'--tw-prose-links-hover': 'var(--tw-prose-invert-links-hover)',
|
||||
'--tw-prose-links-underline': 'var(--tw-prose-invert-links-underline)',
|
||||
'--tw-prose-bold': 'var(--tw-prose-invert-bold)',
|
||||
'--tw-prose-counters': 'var(--tw-prose-invert-counters)',
|
||||
'--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
|
||||
'--tw-prose-hr': 'var(--tw-prose-invert-hr)',
|
||||
'--tw-prose-quotes': 'var(--tw-prose-invert-quotes)',
|
||||
'--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
|
||||
'--tw-prose-captions': 'var(--tw-prose-invert-captions)',
|
||||
'--tw-prose-code': 'var(--tw-prose-invert-code)',
|
||||
'--tw-prose-code-bg': 'var(--tw-prose-invert-code-bg)',
|
||||
'--tw-prose-code-ring': 'var(--tw-prose-invert-code-ring)',
|
||||
'--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
|
||||
'--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PromptVariable } from '@/types/app'
|
||||
|
||||
export function replaceVarWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
|
||||
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
||||
const name = inputs[key]
|
||||
if (name)
|
||||
return name
|
||||
|
||||
const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
|
||||
return valueObj ? `{{${valueObj.key}}}` : match
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
|
||||
export function randomString(length: number) {
|
||||
let result = ''
|
||||
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user