Compare commits

..

27 Commits

Author SHA1 Message Date
bf49c3c15d add missing code 2023-08-28 18:21:38 +08:00
7305de467e feat: add passport code 2023-08-28 18:21:13 +08:00
4fa6b2c2bd just add 2023-08-28 18:20:07 +08:00
a96399ee15 Merge pull request #22 from langgenius/fix/fix-parse-conversation-error
fix: log message json pares error
2023-08-28 17:34:03 +08:00
e8294d2e38 fix: log message json pares error 2023-08-28 17:31:04 +08:00
c2d8c9010a fix: build error on local 2023-07-31 18:31:18 +08:00
3d0958584c feat: api add eventtype pong message to avoid api return too slow and frontend ignore it 2023-07-27 10:34:57 +08:00
d32faf12f2 fix: app update var break old conversation 2023-07-05 11:34:05 +08:00
1ba3f24dc1 Update README.md 2023-06-14 16:38:59 +08:00
a8be513d4b feat: support steaming 2023-06-10 14:28:27 +08:00
cfd0c9532f feat: lint code 2023-06-10 14:04:40 +08:00
2e46f795a4 Merge pull request #4 from jingjingxinshang/fix
Fix 基于前端模版开发时可配置调用api地址
2023-05-18 10:27:08 +08:00
d306b26486 Update common.ts 2023-05-18 10:24:44 +08:00
223ba26902 Update common.ts
ChatClient add API_URL
2023-05-18 09:58:41 +08:00
c4f6cdb7ae Update index.ts
add API_URL
2023-05-18 09:57:37 +08:00
87a063c354 fix: res is not json 2023-05-15 17:48:41 +08:00
b35aa0b18a fix: nodejs endpoint error 2023-05-15 17:11:48 +08:00
cc77dbe1d8 fix: use npm install can not resolved package 2023-05-15 16:37:28 +08:00
342e068022 fix: npm package bug 2023-05-14 17:21:41 +08:00
bb2d1033c6 fix: dify version 2023-05-14 16:59:58 +08:00
885884f1de chore: package to dify-client 2023-05-14 16:57:54 +08:00
0630f6f2da feat: vercel no api cache 2023-05-14 16:52:21 +08:00
7a1b139f04 feat: add ai thinking anim 2023-05-12 14:16:53 +08:00
caf5b31ccd chore: revert changing title 2023-05-12 11:04:50 +08:00
d8cfcc9eae fix: input method enter also send message 2023-05-12 11:04:50 +08:00
f5432694b9 Update README.md 2023-05-11 10:28:46 +08:00
afe399bfd4 Merge pull request #3 from langgenius/feature/dockerfile
feat: add dockerfile
2023-05-11 10:27:43 +08:00
44 changed files with 470 additions and 257 deletions

View File

@ -25,4 +25,4 @@
],
"react-hooks/exhaustive-deps": "warn"
}
}
}

1
.gitignore vendored
View File

@ -17,6 +17,7 @@
# misc
.DS_Store
.vscode
*.pem
# debug

2
.vscode/launch.json vendored
View File

@ -25,4 +25,4 @@
}
}
]
}
}

View File

@ -29,4 +29,4 @@
"i18n/lang",
"app/api/messages"
]
}
}

View File

@ -39,6 +39,7 @@ yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Using Docker
@ -61,6 +62,9 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next
## Deploy on Vercel
> ⚠️ If you are using [Vercel Hobby](https://vercel.com/pricing), your message will be trucated due to the limitation of 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.

View File

@ -1,6 +1,5 @@
import { type NextRequest } from 'next/server'
import { getInfo, client } from '@/app/api/utils/common'
import { OpenAIStream } from '@/app/api/utils/stream'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest) {
const body = await request.json()
@ -8,10 +7,9 @@ export async function POST(request: NextRequest) {
inputs,
query,
conversation_id: conversationId,
response_mode: responseMode
response_mode: responseMode,
} = body
const { user } = getInfo(request);
const { user } = getInfo(request)
const res = await client.createChatMessage(inputs, query, user, responseMode, conversationId)
const stream = await OpenAIStream(res as any)
return new Response(stream as any)
}
return new Response(res.data as any)
}

View File

@ -1,11 +1,15 @@
import { type NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { getInfo, setSession, client } from '@/app/api/utils/common'
import { client, getInfo, setSession } from '@/app/api/utils/common'
export async function GET(request: NextRequest) {
const { sessionId, user } = getInfo(request);
const { data }: any = await client.getConversations(user);
return NextResponse.json(data, {
headers: setSession(sessionId)
})
}
const { sessionId, user } = getInfo(request)
try {
const { data }: any = await client.getConversations(user)
return NextResponse.json(data, {
headers: setSession(sessionId),
})
} catch (error) {
return NextResponse.json([]);
}
}

View File

@ -1,16 +1,16 @@
import { type NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { getInfo, client } from '@/app/api/utils/common'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest, { params }: {
params: { messageId: string }
}) {
const body = await request.json()
const {
rating
rating,
} = body
const { messageId } = params
const { user } = getInfo(request);
const { user } = getInfo(request)
const { data } = await client.messageFeedback(messageId, rating, user)
return NextResponse.json(data)
}

View File

@ -1,13 +1,13 @@
import { type NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { getInfo, setSession, client } from '@/app/api/utils/common'
import { client, getInfo, setSession } from '@/app/api/utils/common'
export async function GET(request: NextRequest) {
const { sessionId, user } = getInfo(request);
const { searchParams } = new URL(request.url);
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);
const { data }: any = await client.getConversationMessages(user, conversationId as string)
return NextResponse.json(data, {
headers: setSession(sessionId)
headers: setSession(sessionId),
})
}
}

View File

@ -1,11 +1,15 @@
import { type NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { getInfo, setSession, client } from '@/app/api/utils/common'
import { client, getInfo, setSession } 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)
})
}
const { sessionId, user } = getInfo(request)
try {
const { data } = await client.getApplicationParameters(user)
return NextResponse.json(data as object, {
headers: setSession(sessionId),
})
} catch (error) {
return NextResponse.json([]);
}
}

21
app/api/passport/route.ts Normal file
View File

@ -0,0 +1,21 @@
import { type NextRequest } from 'next/server'
import { client } from '@/app/api/utils/common'
import { API_KEY, API_URL, APP_ID } from '@/config'
// import { commonClient } from 'dify-client'
import axios from "axios";
export async function GET(request: NextRequest) {
const headers = {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
'X-App-Code': APP_ID
};
const res = await axios({
url: 'https://api.dify.ai/v1/passport',
headers,
responseType: "json",
})
console.log(res)
return new Response(res.data)
}

View File

@ -1,16 +1,16 @@
import { type NextRequest } from 'next/server'
import { APP_ID, API_KEY } from '@/config'
import { ChatClient } from 'langgenius-client'
import { ChatClient, DifyClient } from 'dify-client'
import { v4 } from 'uuid'
import { API_KEY, API_URL, APP_ID } from '@/config'
const userPrefix = `user_${APP_ID}:`;
const userPrefix = `user_${APP_ID}:`
export const getInfo = (request: NextRequest) => {
const sessionId = request.cookies.get('session_id')?.value || v4();
const user = userPrefix + sessionId;
const sessionId = request.cookies.get('session_id')?.value || v4()
const user = userPrefix + sessionId
return {
sessionId,
user
user,
}
}
@ -18,4 +18,5 @@ export const setSession = (sessionId: string) => {
return { 'Set-Cookie': `session_id=${sessionId}` }
}
export const client = new ChatClient(API_KEY)
export const client = new ChatClient(API_KEY, API_URL || undefined)
export const commonClient = new ChatClient(API_KEY, API_URL || undefined)

View File

@ -1,25 +0,0 @@
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;
}

View File

@ -14,9 +14,8 @@ const AppUnavailable: FC<IAppUnavailableProps> = ({
}) => {
const { t } = useTranslation()
let message = errMessage
if (!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'>

View File

@ -19,6 +19,7 @@ const AutoHeightTextarea = forwardRef(
{ value, onChange, placeholder, className, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps,
outerRef: any,
) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const ref = outerRef || useRef<HTMLTextAreaElement>(null)
const doFocus = () => {

View File

@ -18,7 +18,7 @@ export function Markdown(props: { content: string }) {
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match
return (!inline && match)
? (
<SyntaxHighlighter
{...props}

View File

@ -1,10 +1,11 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import React, { useEffect, useRef } from 'react'
import cn from 'classnames'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import LoadingAnim from './loading-anim'
import { randomString } from '@/utils/string'
import type { Feedbacktype, MessageRating } from '@/types/app'
import Tooltip from '@/app/components/base/tooltip'
@ -166,7 +167,13 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback,
return (
<div key={id}>
<div className='flex items-start'>
<div className={`${s.answerIcon} ${isResponsing ? s.typeingIcon : ''} w-10 h-10 shrink-0`}></div>
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
{isResponsing
&& <div className={s.typeingIcon}>
<LoadingAnim type='avatar' />
</div>
}
</div>
<div className={`${s.answerWrap}`}>
<div className={`${s.answer} relative text-sm text-gray-900`}>
<div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}>
@ -176,7 +183,15 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback,
<div className='text-xs text-gray-500'>{t('app.chat.openingStatementTitle')}</div>
</div>
)}
<Markdown content={content} />
{(isResponsing && !content)
? (
<div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' />
</div>
)
: (
<Markdown content={content} />
)}
</div>
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()}
@ -232,6 +247,7 @@ const Chat: FC<IChatProps> = ({
}) => {
const { t } = useTranslation()
const { notify } = Toast
const isUseInputMethod = useRef(false)
const [query, setQuery] = React.useState('')
const handleContentChange = (e: any) => {
@ -267,12 +283,14 @@ const Chat: FC<IChatProps> = ({
const handleKeyUp = (e: any) => {
if (e.code === 'Enter') {
e.preventDefault()
if (!e.shiftKey)
// prevent send message when using input method enter
if (!e.shiftKey && !isUseInputMethod.current)
handleSend()
}
}
const haneleKeyDown = (e: any) => {
isUseInputMethod.current = e.nativeEvent.isComposing
if (e.code === 'Enter' && !e.shiftKey) {
setQuery(query.replace(/\n$/, ''))
e.preventDefault()

View File

@ -0,0 +1,17 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
export type ILoaidingAnimProps = {
type: 'text' | 'avatar'
}
const LoaidingAnim: FC<ILoaidingAnimProps> = ({
type,
}) => {
return (
<div className={`${s['dot-flashing']} ${s[type]}`}></div>
)
}
export default React.memo(LoaidingAnim)

View File

@ -0,0 +1,82 @@
.dot-flashing {
position: relative;
animation: 1s infinite linear alternate;
animation-delay: 0.5s;
}
.dot-flashing::before,
.dot-flashing::after {
content: "";
display: inline-block;
position: absolute;
top: 0;
animation: 1s infinite linear alternate;
}
.dot-flashing::before {
animation-delay: 0s;
}
.dot-flashing::after {
animation-delay: 1s;
}
@keyframes dot-flashing {
0% {
background-color: #667085;
}
50%,
100% {
background-color: rgba(102, 112, 133, 0.3);
}
}
@keyframes dot-flashing-avatar {
0% {
background-color: #155EEF;
}
50%,
100% {
background-color: rgba(21, 94, 239, 0.3);
}
}
.text,
.text::before,
.text::after {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #667085;
color: #667085;
animation-name: dot-flashing;
}
.text::before {
left: -7px;
}
.text::after {
left: 7px;
}
.avatar,
.avatar::before,
.avatar::after {
width: 2px;
height: 2px;
border-radius: 50%;
background-color: #155EEF;
color: #155EEF;
animation-name: dot-flashing-avatar;
}
.avatar::before {
left: -5px;
}
.avatar::after {
left: 5px;
}

View File

@ -1,22 +1,23 @@
.answerIcon {
position: relative;
background: url(./icons/robot.svg);
}
.typeingIcon {
position: relative;
}
.typeingIcon::after {
content: '';
position: absolute;
top: -3px;
left: -3px;
top: 0px;
left: 0px;
display: flex;
justify-content: center;
align-items: center;
width: 16px;
height: 16px;
background: url(./icons/typing.svg) no-repeat;
background-size: contain;
background: #FFFFFF;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
border-radius: 16px;
}
.questionIcon {
background: url(./icons/default-avatar.jpg);
background-size: contain;

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
@ -10,15 +11,15 @@ import Sidebar from '@/app/components/sidebar'
import ConfigSence from '@/app/components/config-scence'
import Header from '@/app/components/header'
import { fetchAppParams, fetchChatList, fetchConversations, sendChatMessage, updateFeedback } from '@/service'
import type { ConversationItem, Feedbacktype, IChatItem, PromptConfig, AppInfo } from '@/types/app'
import type { ConversationItem, Feedbacktype, IChatItem, PromptConfig } from '@/types/app'
import Chat from '@/app/components/chat'
import { setLocaleOnClient } from '@/i18n/client'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Loading from '@/app/components/base/loading'
import { replaceVarWithValues } from '@/utils/prompt'
import { replaceVarWithValues, userInputsFormToPromptVariables } from '@/utils/prompt'
import AppUnavailable from '@/app/components/app-unavailable'
import { APP_ID, API_KEY, APP_INFO, isShowPrompt, promptTemplate } from '@/config'
import { userInputsFormToPromptVariables } from '@/utils/prompt'
import { API_KEY, APP_ID, APP_INFO, isShowPrompt, promptTemplate } from '@/config'
import { checkOrSetAccessToken } from '@/utils/access-token'
const Main: FC = () => {
const { t } = useTranslation()
@ -37,9 +38,8 @@ const Main: FC = () => {
const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false)
useEffect(() => {
if (APP_INFO?.title) {
if (APP_INFO?.title)
document.title = `${APP_INFO.title} - Powered by Dify`
}
}, [APP_INFO?.title])
/*
@ -198,6 +198,8 @@ const Main: FC = () => {
return
}
(async () => {
await checkOrSetAccessToken()
try {
const [conversationData, appParams] = await Promise.all([fetchConversations(), fetchAppParams()])
@ -245,6 +247,9 @@ const Main: FC = () => {
}
const checkCanSend = () => {
if (currConversationId !== '-1')
return true
if (!currInputs || !promptConfig?.prompt_variables)
return true
@ -282,7 +287,7 @@ const Main: FC = () => {
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
const placeholderAnswerItem = {
id: placeholderAnswerId,
content: '...',
content: '',
isAnswer: true,
}
@ -318,9 +323,9 @@ const Main: FC = () => {
},
async onCompleted() {
setResponsingFalse()
if (!tempNewConversationId) {
if (!tempNewConversationId)
return
}
if (getConversationIdChangeBecauseOfNew()) {
const { data: conversations }: any = await fetchConversations()
setConversationList(conversations as ConversationItem[])

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel'
import s from './style.module.css'
import { AppInfoComp, ChatBtn, EditBtn, FootLogo, PromptTemplate } from './massive-component'
import type { PromptConfig, AppInfo } from '@/types/app'
import type { AppInfo, PromptConfig } from '@/types/app'
import Toast from '@/app/components/base/toast'
import Select from '@/app/components/base/select'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
@ -276,7 +276,7 @@ const Welcome: FC<IWelcomeProps> = ({
}
const renderHasSetInputs = () => {
if (!isPublicVersion && !canEidtInpus || !hasVar)
if ((!isPublicVersion && !canEidtInpus) || !hasVar)
return null
return (

2
app/global.d.ts vendored
View File

@ -1,2 +1,2 @@
declare module 'langgenius-client';
declare module 'dify-client';
declare module 'uuid';

View File

@ -1,19 +1,19 @@
import { AppInfo } from "@/types/app"
import type { AppInfo } from '@/types/app'
export const APP_ID = ''
export const API_KEY = ''
export const API_URL = ''
export const APP_INFO: AppInfo = {
"title": 'Chat APP',
"description": '',
"copyright": '',
"privacy_policy": '',
"default_language": 'zh-Hans'
title: 'Chat APP',
description: '',
copyright: '',
privacy_policy: '',
default_language: 'zh-Hans',
}
export const isShowPrompt = false
export const promptTemplate = 'I want you to act as a javascript console.'
export const API_PREFIX = '/api';
export const API_PREFIX = '/api'
export const LOCALE_COOKIE_NAME = 'locale'

View File

@ -8,20 +8,22 @@ export enum MediaType {
}
const useBreakpoints = () => {
const [width, setWidth] = React.useState(globalThis.innerWidth);
const [width, setWidth] = React.useState(globalThis.innerWidth)
const media = (() => {
if (width <= 640) return MediaType.mobile;
if (width <= 768) return MediaType.tablet;
return MediaType.pc;
})();
if (width <= 640)
return MediaType.mobile
if (width <= 768)
return MediaType.tablet
return MediaType.pc
})()
React.useEffect(() => {
const handleWindowResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleWindowResize);
return () => window.removeEventListener("resize", handleWindowResize);
}, []);
const handleWindowResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleWindowResize)
return () => window.removeEventListener('resize', handleWindowResize)
}, [])
return media;
return media
}
export default useBreakpoints
export default useBreakpoints

View File

@ -1,6 +1,6 @@
import { useState } from 'react'
import type { ConversationItem } from '@/types/app'
import produce from 'immer'
import type { ConversationItem } from '@/types/app'
const storageConversationIdKey = 'conversationIdInfo'
@ -29,9 +29,10 @@ function useConversation() {
// input can be updated by user
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null)
const resetNewConversationInputs = () => {
if (!newConversationInputs) return
setNewConversationInputs(produce(newConversationInputs, draft => {
Object.keys(draft).forEach(key => {
if (!newConversationInputs)
return
setNewConversationInputs(produce(newConversationInputs, (draft) => {
Object.keys(draft).forEach((key) => {
draft[key] = ''
})
}))
@ -59,8 +60,8 @@ function useConversation() {
setCurrInputs,
currConversationInfo,
setNewConversationInfo,
setExistConversationInfo
setExistConversationInfo,
}
}
export default useConversation;
export default useConversation

View File

@ -12,7 +12,6 @@ export const getLocaleOnClient = (): Locale => {
export const setLocaleOnClient = (locale: Locale, notReload?: boolean) => {
Cookies.set(LOCALE_COOKIE_NAME, locale)
changeLanguage(locale)
if (!notReload) {
if (!notReload)
location.reload()
}
}

View File

@ -5,7 +5,7 @@ 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 '.'
import type { Locale } from '.'
const resources = {
'en': {

View File

@ -1,7 +1,7 @@
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import { Locale } from '.'
import type { Locale } from '.'
// https://locize.com/blog/next-13-app-dir-i18n/
const initI18next = async (lng: Locale, ns: string) => {
@ -21,6 +21,6 @@ export async function useTranslation(lng: Locale, ns = '', options: Record<strin
const i18nextInstance = await initI18next(lng, ns)
return {
t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix),
i18n: i18nextInstance
i18n: i18nextInstance,
}
}
}

View File

@ -1,33 +1,33 @@
const translation = {
common: {
welcome: "Welcome to use",
appUnavailable: "App is unavailable",
appUnkonwError: "App is unavailable"
welcome: 'Welcome to use',
appUnavailable: 'App is unavailable',
appUnkonwError: 'App is unavailable',
},
chat: {
newChat: "New chat",
newChatDefaultName: "New conversation",
openingStatementTitle: "Opening statement",
powerBy: "Powered by",
prompt: "Prompt",
privatePromptConfigTitle: "Conversation settings",
publicPromptConfigTitle: "Initial Prompt",
configStatusDes: "Before start, you can modify conversation settings",
newChat: 'New chat',
newChatDefaultName: 'New conversation',
openingStatementTitle: 'Opening statement',
powerBy: 'Powered by',
prompt: 'Prompt',
privatePromptConfigTitle: 'Conversation settings',
publicPromptConfigTitle: 'Initial Prompt',
configStatusDes: 'Before start, you can modify conversation settings',
configDisabled:
"Previous session settings have been used for this session.",
startChat: "Start Chat",
'Previous session settings have been used for this session.',
startChat: 'Start Chat',
privacyPolicyLeft:
"Please read the ",
'Please read the ',
privacyPolicyMiddle:
"privacy policy",
'privacy policy',
privacyPolicyRight:
" provided by the app developer.",
' provided by the app developer.',
},
errorMessage: {
valueOfVarRequired: "Variables value can not be empty",
valueOfVarRequired: 'Variables value can not be empty',
waitForResponse:
"Please wait for the response to the previous message to complete.",
'Please wait for the response to the previous message to complete.',
},
};
}
export default translation;
export default translation

View File

@ -1,28 +1,28 @@
const translation = {
common: {
welcome: "欢迎使用",
appUnavailable: "应用不可用",
appUnkonwError: "应用不可用",
welcome: '欢迎使用',
appUnavailable: '应用不可用',
appUnkonwError: '应用不可用',
},
chat: {
newChat: "新对话",
newChatDefaultName: "新的对话",
openingStatementTitle: "对话开场白",
powerBy: "Powered by",
prompt: "提示词",
privatePromptConfigTitle: "对话设置",
publicPromptConfigTitle: "对话前提示词",
configStatusDes: "开始前,您可以修改对话设置",
configDisabled: "此次会话已使用上次会话表单",
startChat: "开始对话",
privacyPolicyLeft: "请阅读由该应用开发者提供的",
privacyPolicyMiddle: "隐私政策",
privacyPolicyRight: "。",
newChat: '新对话',
newChatDefaultName: '新的对话',
openingStatementTitle: '对话开场白',
powerBy: 'Powered by',
prompt: '提示词',
privatePromptConfigTitle: '对话设置',
publicPromptConfigTitle: '对话前提示词',
configStatusDes: '开始前,您可以修改对话设置',
configDisabled: '此次会话已使用上次会话表单',
startChat: '开始对话',
privacyPolicyLeft: '请阅读由该应用开发者提供的',
privacyPolicyMiddle: '隐私政策',
privacyPolicyRight: '。',
},
errorMessage: {
valueOfVarRequired: "变量值必填",
waitForResponse: "请等待上条信息响应完成",
valueOfVarRequired: '变量值必填',
waitForResponse: '请等待上条信息响应完成',
},
};
}
export default translation;
export default translation

View File

@ -16,7 +16,7 @@ const translation = {
lineBreak: 'Line break',
like: 'like',
dislike: 'dislike',
}
},
}
export default translation

View File

@ -16,7 +16,7 @@ const translation = {
lineBreak: '换行',
like: '赞同',
dislike: '反对',
}
},
}
export default translation

View File

@ -15,7 +15,7 @@ const nextConfig = {
typescript: {
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
ignoreBuildErrors: true,
}
},
}
module.exports = nextConfig

View File

@ -7,7 +7,9 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"fix": "next lint --fix"
"fix": "next lint --fix",
"eslint-fix": "eslint . --fix",
"prepare": "husky install ./.husky"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.2.32",
@ -24,16 +26,18 @@
"axios": "^1.3.5",
"classnames": "^2.3.2",
"copy-to-clipboard": "^3.3.3",
"dify-client": "2.0.0",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"eslint-config-next": "13.4.0",
"eventsource-parser": "^1.0.0",
"husky": "^8.0.3",
"i18next": "^22.4.13",
"i18next-resources-to-backend": "^1.1.3",
"immer": "^9.0.19",
"js-cookie": "^3.0.1",
"langgenius-client": "1.1.1",
"katex": "^0.16.7",
"negotiator": "^0.6.3",
"next": "13.2.4",
"next": "13.4.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.2",
@ -55,15 +59,23 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@antfu/eslint-config": "^0.36.0",
"@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",
"lint-staged": "^13.2.2",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7"
},
"lint-staged": {
"**/*.js?(x)": [
"eslint --fix"
],
"**/*.ts?(x)": [
"eslint --fix"
]
}
}
}

View File

@ -1,4 +1,4 @@
import { API_PREFIX } from '@/config'
import { API_PREFIX, APP_ID } from '@/config'
import Toast from '@/app/components/base/toast'
const TIME_OUT = 100000
@ -62,8 +62,21 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
const lines = buffer.split('\n')
try {
lines.forEach((message) => {
if (!message) return
bufferObj = JSON.parse(message) // remove data: and parse as json
if (!message || !message.startsWith('data: '))
return
try {
bufferObj = JSON.parse(message.substring(6)) // remove data: and parse as json
} catch (e) {
// mute handle message cut off
onData('', isFirstMessage, {
conversationId: bufferObj?.conversation_id,
messageId: bufferObj?.id,
})
return
}
if (bufferObj.event !== 'message')
return
onData(unicodeToChar(bufferObj.answer), isFirstMessage, {
conversationId: bufferObj.conversation_id,
messageId: bufferObj.id,
@ -71,11 +84,12 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
isFirstMessage = false
})
buffer = lines[lines.length - 1]
} catch (e) {
}
catch (e) {
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: e + ''
errorMessage: `${e}`,
})
return
}
@ -88,8 +102,17 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
const baseFetch = (url: string, fetchOptions: any, { needAllResponseContent }: IOtherOptions) => {
const options = Object.assign({}, baseOptions, fetchOptions)
const sharedToken = APP_ID
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
let accessTokenJson = { [sharedToken]: '' }
try {
accessTokenJson = JSON.parse(accessToken)
}
catch (e) {
let urlPrefix = API_PREFIX
}
options.headers.set('Authorization', `Bearer ${accessTokenJson[sharedToken]}`)
const urlPrefix = API_PREFIX
let urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`
@ -125,27 +148,32 @@ const baseFetch = (url: string, fetchOptions: any, { needAllResponseContent }: I
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 })
try {
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 })
})
})
})
}
}
catch (e) {
Toast.notify({ type: 'error', message: `${e}` })
}
return Promise.reject(resClone)
}
// handle delete api. Delete api not return content.
if (res.status === 204) {
resolve({ result: "success" })
resolve({ result: 'success' })
return
}
@ -162,8 +190,7 @@ const baseFetch = (url: string, fetchOptions: any, { needAllResponseContent }: I
])
}
export const ssePost = (url: string, fetchOptions: any, {
onData, onCompleted, onError }: IOtherOptions) => {
export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, onError }: IOtherOptions) => {
const options = Object.assign({}, baseOptions, {
method: 'POST',
}, fetchOptions)

View File

@ -31,3 +31,7 @@ export const fetchAppParams = async () => {
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
return post(url, { body })
}
export const fetchAccessToken = async (appId: string) => {
return get('/passport') as Promise<{ access_token: string }>
}

17
service/vercel.json Normal file
View File

@ -0,0 +1,17 @@
{
"headers": [
{
"source": "/api/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "no-store, max-age=0"
},
{
"key": "Pragma",
"value": "no-cache"
}
]
}
]
}

View File

@ -46,15 +46,15 @@ module.exports = {
indigo: {
25: '#F5F8FF',
100: '#E0EAFF',
600: '#444CE7'
}
600: '#444CE7',
},
},
screens: {
'mobile': '100px',
mobile: '100px',
// => @media (min-width: 100px) { ... }
'tablet': '640px', // 391
tablet: '640px', // 391
// => @media (min-width: 600px) { ... }
'pc': '769px',
pc: '769px',
// => @media (min-width: 769px) { ... }
},
},

View File

@ -40,4 +40,4 @@
"exclude": [
"node_modules"
]
}
}

View File

@ -1,31 +1,31 @@
import { Locale } from '@/i18n'
import type { Locale } from '@/i18n'
export type PromptVariable = {
key: string,
name: string,
type: "string" | "number" | "select",
default?: string | number,
key: string
name: string
type: 'string' | 'number' | 'select'
default?: string | number
options?: string[]
max_length?: number
required: boolean
}
export type PromptConfig = {
prompt_template: string,
prompt_variables: PromptVariable[],
prompt_template: string
prompt_variables: PromptVariable[]
}
export type TextTypeFormItem = {
label: string,
variable: string,
label: string
variable: string
required: boolean
max_length: number
}
export type SelectTypeFormItem = {
label: string,
variable: string,
required: boolean,
label: string
variable: string
required: boolean
options: string[]
}
/**
@ -79,14 +79,13 @@ export type IChatItem = {
isOpeningStatement?: boolean
}
export type ResponseHolder = {}
export type ConversationItem = {
id: string
name: string
inputs: Record<string, any> | null
introduction: string,
introduction: string
}
export type AppInfo = {
@ -95,4 +94,4 @@ export type AppInfo = {
default_language: Locale
copyright?: string
privacy_policy?: string
}
}

View File

@ -38,15 +38,15 @@ module.exports = ({ theme }) => ({
'--tw-prose-invert-td-borders': theme('colors.zinc.700'),
// Base
color: 'var(--tw-prose-body)',
fontSize: theme('fontSize.sm')[0],
lineHeight: theme('lineHeight.7'),
'color': 'var(--tw-prose-body)',
'fontSize': theme('fontSize.sm')[0],
'lineHeight': theme('lineHeight.7'),
// Layout
'> *': {
maxWidth: theme('maxWidth.2xl'),
marginLeft: 'auto',
marginRight: 'auto',
'maxWidth': theme('maxWidth.2xl'),
'marginLeft': 'auto',
'marginRight': 'auto',
'@screen lg': {
maxWidth: theme('maxWidth.3xl'),
marginLeft: `calc(50% - min(50%, ${theme('maxWidth.lg')}))`,
@ -55,7 +55,7 @@ module.exports = ({ theme }) => ({
},
// Text
p: {
'p': {
marginTop: theme('spacing.6'),
marginBottom: theme('spacing.6'),
},
@ -65,7 +65,7 @@ module.exports = ({ theme }) => ({
},
// Lists
ol: {
'ol': {
listStyleType: 'decimal',
marginTop: theme('spacing.5'),
marginBottom: theme('spacing.5'),
@ -98,13 +98,13 @@ module.exports = ({ theme }) => ({
'ol[type="1"]': {
listStyleType: 'decimal',
},
ul: {
'ul': {
listStyleType: 'disc',
marginTop: theme('spacing.5'),
marginBottom: theme('spacing.5'),
paddingLeft: '1.625rem',
},
li: {
'li': {
marginTop: theme('spacing.2'),
marginBottom: theme('spacing.2'),
},
@ -140,14 +140,14 @@ module.exports = ({ theme }) => ({
},
// 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')})`,
'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')})`,
@ -159,7 +159,7 @@ module.exports = ({ theme }) => ({
},
// Quotes
blockquote: {
'blockquote': {
fontWeight: '500',
fontStyle: 'italic',
color: 'var(--tw-prose-quotes)',
@ -178,14 +178,14 @@ module.exports = ({ theme }) => ({
},
// Headings
h1: {
'h1': {
color: 'var(--tw-prose-headings)',
fontWeight: '700',
fontSize: theme('fontSize.2xl')[0],
...theme('fontSize.2xl')[1],
marginBottom: theme('spacing.2'),
},
h2: {
'h2': {
color: 'var(--tw-prose-headings)',
fontWeight: '600',
fontSize: theme('fontSize.lg')[0],
@ -193,7 +193,7 @@ module.exports = ({ theme }) => ({
marginTop: theme('spacing.16'),
marginBottom: theme('spacing.2'),
},
h3: {
'h3': {
color: 'var(--tw-prose-headings)',
fontSize: theme('fontSize.base')[0],
...theme('fontSize.base')[1],
@ -211,7 +211,7 @@ module.exports = ({ theme }) => ({
marginTop: '0',
marginBottom: '0',
},
figcaption: {
'figcaption': {
color: 'var(--tw-prose-captions)',
fontSize: theme('fontSize.xs')[0],
...theme('fontSize.xs')[1],
@ -219,7 +219,7 @@ module.exports = ({ theme }) => ({
},
// Tables
table: {
'table': {
width: '100%',
tableLayout: 'auto',
textAlign: 'left',
@ -227,7 +227,7 @@ module.exports = ({ theme }) => ({
marginBottom: theme('spacing.8'),
lineHeight: theme('lineHeight.6'),
},
thead: {
'thead': {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-th-borders)',
},
@ -255,7 +255,7 @@ module.exports = ({ theme }) => ({
'tbody td': {
verticalAlign: 'baseline',
},
tfoot: {
'tfoot': {
borderTopWidth: '1px',
borderTopColor: 'var(--tw-prose-th-borders)',
},
@ -276,13 +276,13 @@ module.exports = ({ theme }) => ({
},
// 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'),
'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)',
@ -291,14 +291,14 @@ module.exports = ({ theme }) => ({
':is(h1, h2, h3) a': {
fontWeight: 'inherit',
},
strong: {
'strong': {
color: 'var(--tw-prose-bold)',
fontWeight: '600',
},
':is(a, blockquote, thead th) strong': {
color: 'inherit',
},
code: {
'code': {
color: 'var(--tw-prose-code)',
borderRadius: theme('borderRadius.lg'),
paddingTop: theme('padding.1'),

19
utils/access-token.ts Normal file
View File

@ -0,0 +1,19 @@
import { fetchAccessToken } from '@/service'
import { APP_ID } from '@/config'
export const checkOrSetAccessToken = async () => {
const sharedToken = APP_ID
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
let accessTokenJson = { [sharedToken]: '' }
try {
accessTokenJson = JSON.parse(accessToken)
}
catch (e) {
}
if (!accessTokenJson[sharedToken]) {
const res = await fetchAccessToken(sharedToken)
accessTokenJson[sharedToken] = res.access_token
localStorage.setItem('token', JSON.stringify(accessTokenJson))
}
}

View File

@ -1,4 +1,4 @@
import { PromptVariable, UserInputFormItem } from '@/types/app'
import type { PromptVariable, UserInputFormItem } from '@/types/app'
export function replaceVarWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
@ -12,11 +12,12 @@ export function replaceVarWithValues(str: string, promptVariables: PromptVariabl
}
export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | null) => {
if (!useInputs) return []
if (!useInputs)
return []
const promptVariables: PromptVariable[] = []
useInputs.forEach((item: any) => {
const type = item['text-input'] ? 'string' : 'select'
const content = type === 'string' ? item['text-input'] : item['select']
const content = type === 'string' ? item['text-input'] : item.select
if (type === 'string') {
promptVariables.push({
key: content.variable,
@ -26,7 +27,8 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
max_length: content.max_length,
options: [],
})
} else {
}
else {
promptVariables.push({
key: content.variable,
name: content.label,
@ -37,4 +39,4 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
}
})
return promptVariables
}
}