Compare commits

...

10 Commits

Author SHA1 Message Date
d8520e72ec 在useEffect hooks中传递第二个参数 空数组,尝试解决 Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. 报错问题 2025-07-29 15:28:14 +08:00
e24a04098c 修改应用名称 2025-07-24 15:39:37 +08:00
cf434cf930 将配置文件拆分成开发与生产环境 2025-07-24 15:11:38 +08:00
38f7b2d550 修改Dockerfile中pnpm镜像源,加快依赖拉去速度 2025-07-24 13:13:24 +08:00
7b81f6b824 Dockerfile文件增加全局安装pnpm 2025-07-24 13:03:34 +08:00
b76320a3e7 提交pnpm-lock.yaml文件,问答助手反馈弹窗再次弹出时,清空之前填写的内容 2025-07-24 10:54:50 +08:00
26cd7f7e8f 问答助手完成子组件反对填写反馈建议模态框确认按钮loading加载,增加confirmLoading状态,并改确认事件为异步,传递至父组件
问答助手解决Popconfirm气泡确认框不触发的问题,为Dify二次开发问答助手前端的Tooltip导致及Popconfirm需在Button按钮的上一级才可触发,将Dify的Tooltip更换成Ant Design的Tooltip,此气泡确认框的作用为取消赞成及取消反对时进行确认,而不是直接取消
2025-07-23 17:28:10 +08:00
36f93db93f 问答助手反馈解决赞同类型记录不了的问题,是由于代码中只针对反对类型进行了记录,并增加 isSubmittingNow 是否提交中的state,已经使用useEffect这个React的hook进行监听状态变化,解决React 的状态更新是 异步的 ,而且函数闭包会捕获旧值,导致提交到后台接口中的反馈内容、消息ID、会话ID获取到的是之前的问题,并完成取消赞同和取消反对功能 2025-07-22 17:02:55 +08:00
d2d96ab315 Dockerfile打包使用pnpm
next.js打包忽略eslint和typescript报错导致打包失败的问题
ant-design引入兼容包,以兼容React19
eslint配置文件由json更换成js
反馈内容由写死的改为获取当前的conversationid会话ID,反馈的用户名称username从cookie里获取
反馈弹窗弹出时使用Promise异步等待如果没有点击确定,则不传递给dify已反馈成功请求
2025-07-14 16:26:45 +08:00
d1a47297bf 本地开发反向代理 2025-07-10 16:57:54 +08:00
30 changed files with 10922 additions and 283 deletions

8
.env.development Normal file
View File

@ -0,0 +1,8 @@
# APP ID
NEXT_PUBLIC_APP_ID=c36db3aa-20a6-4421-8e62-c9af03020088
# APP API key
NEXT_PUBLIC_APP_KEY=app-WN8APQeButq9jIRj2g5D7r22
# API url prefix
NEXT_PUBLIC_API_URL=http://192.168.6.35:180/v1
NEXT_PUBLIC_BASE_API_URL=http://127.0.0.1:8085

View File

@ -1,8 +0,0 @@
# APP ID
NEXT_PUBLIC_APP_ID=8f005d85-a520-4b64-82d7-09f76104cc80
# APP API key
NEXT_PUBLIC_APP_KEY=app-9o5BfmyoyKiNui9Pe855xasf
# API url prefix
NEXT_PUBLIC_API_URL=http://192.168.6.37:180/v1
NEXT_PUBLIC_BASE_API_URL=http://192.168.6.9:8085

8
.env.production Normal file
View File

@ -0,0 +1,8 @@
# APP ID
NEXT_PUBLIC_APP_ID=c36db3aa-20a6-4421-8e62-c9af03020088
# APP API key
NEXT_PUBLIC_APP_KEY=app-WN8APQeButq9jIRj2g5D7r22
# API url prefix
NEXT_PUBLIC_API_URL=http://192.168.6.35:180/v1
NEXT_PUBLIC_BASE_API_URL=http://192.168.6.35:8088/ruoyi-admin

View File

@ -1,28 +0,0 @@
{
"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"
}
}

2
.gitignore vendored
View File

@ -48,7 +48,7 @@ yarn.lock
.yarnrc.yml .yarnrc.yml
# pmpm # pmpm
pnpm-lock.yaml # pnpm-lock.yaml
# info # info
keys.ts keys.ts

View File

@ -4,9 +4,11 @@ WORKDIR /app
COPY . . COPY . .
RUN yarn install RUN npm install -g pnpm
RUN yarn build RUN npm config set registry https://registry.npmmirror.com
RUN pnpm install
RUN pnpm build
EXPOSE 3000 EXPOSE 3000
CMD ["yarn","start"] CMD ["pnpm","start"]

View File

@ -1,12 +1,17 @@
import { type NextRequest } from 'next/server' import { type NextRequest } from "next/server";
import { NextResponse } from 'next/server' import { NextResponse } from "next/server";
import { client, getInfo } from '@/app/api/utils/common' import { client, getInfo } from "@/app/api/utils/common";
export async function POST(request: NextRequest, { params }: { export async function POST(
params: { taskId: string } request: NextRequest,
}) { {
const { taskId } = await params params,
const { user } = getInfo(request) }: {
const { data } = await client.stopChat(taskId, user) params: Promise<{ taskId: string }>;
return NextResponse.json(data) }
) {
const { taskId } = await params;
const { user } = getInfo(request);
const { data } = await client.stopChat(taskId, user);
return NextResponse.json(data);
} }

View File

@ -1,19 +1,26 @@
import { type NextRequest } from 'next/server' import { type NextRequest } from "next/server";
import { NextResponse } from 'next/server' import { NextResponse } from "next/server";
import { client, getInfo } from '@/app/api/utils/common' import { client, getInfo } from "@/app/api/utils/common";
export async function POST(request: NextRequest, { params }: { export async function POST(
params: { conversationId: string } request: NextRequest,
}) { {
const body = await request.json() params,
const { }: {
auto_generate, params: Promise<{ conversationId: string }>;
name, }
} = body ) {
const { conversationId } = await params const body = await request.json();
const { user } = getInfo(request) const { auto_generate, name } = body;
const { conversationId } = await params;
const { user } = getInfo(request);
// auto generate name // auto generate name
const { data } = await client.renameConversation(conversationId, name, user, auto_generate) const { data } = await client.renameConversation(
return NextResponse.json(data) conversationId,
name,
user,
auto_generate
);
return NextResponse.json(data);
} }

View File

@ -1,13 +1,18 @@
import { type NextRequest } from 'next/server' import { type NextRequest } from "next/server";
import { NextResponse } from 'next/server' import { NextResponse } from "next/server";
import { client, getInfo } from '@/app/api/utils/common' import { client, getInfo } from "@/app/api/utils/common";
export async function DELETE(request: NextRequest, { params }: { export async function DELETE(
params: { conversationId: string } request: NextRequest,
}) { {
const { conversationId } = await params params,
const { user } = await getInfo(request) }: {
params: Promise<{ conversationId: string }>;
}
) {
const { conversationId } = await params;
const { user } = await getInfo(request);
const { data } = await client.deleteConversation(conversationId, user) const { data } = await client.deleteConversation(conversationId, user);
return NextResponse.json(data) return NextResponse.json(data);
} }

View File

@ -1,35 +1,34 @@
import { type NextRequest } from 'next/server' import { type NextRequest } from "next/server";
import { NextResponse } from 'next/server' import { NextResponse } from "next/server";
import { client, getInfo, setSession } from '@/app/api/utils/common' import { client, getInfo, setSession } from "@/app/api/utils/common";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const { sessionId, user } = getInfo(request) const { sessionId, user } = getInfo(request);
try { try {
const { data } = await client.getInfo(user) const { data } = await client.getInfo(user);
// 接口未返回 建议从配置文件或者环境变量获取 // 接口未返回 建议从配置文件或者环境变量获取
data.app_id = 'app_id' data.app_id = "app_id";
data.site = { data.site = {
"title": "卓远智能问答", title: "ERP问答助手",
"chat_color_theme": null, chat_color_theme: null,
"chat_color_theme_inverted": false, chat_color_theme_inverted: false,
"icon_type": "image", icon_type: "image",
"icon": "48159ee8", icon: "48159ee8",
"icon_background": "#E4FBCC", icon_background: "#E4FBCC",
"icon_url": "/files/aaa.png", icon_url: "/files/aaa.png",
"description": "Zhuoyuan AI Helper", description: "Zhuoyuan AI Helper",
"copyright": null, copyright: null,
"privacy_policy": null, privacy_policy: null,
"custom_disclaimer": "", custom_disclaimer: "",
"default_language": "zh-Hans", default_language: "zh-Hans",
"prompt_public": false, prompt_public: false,
"show_workflow_steps": true, show_workflow_steps: true,
"use_icon_as_answer_icon": false use_icon_as_answer_icon: false,
} };
return NextResponse.json(data as object, { return NextResponse.json(data as object, {
headers: setSession(sessionId), headers: setSession(sessionId),
}) });
} } catch (error) {
catch (error) { return NextResponse.json([]);
return NextResponse.json([])
} }
} }

View File

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

View File

@ -1,10 +1,17 @@
import { type NextRequest } from 'next/server' import { type NextRequest } from "next/server";
import { NextResponse } from 'next/server' import { NextResponse } from "next/server";
import { client, getInfo } from '@/app/api/utils/common' import { client, getInfo } from "@/app/api/utils/common";
export async function GET(request: NextRequest, params: { messageId: string }) { export async function GET(
const { messageId } = await params request: NextRequest,
const { user } = getInfo(request) {
const { data }: any = await client.getSuggested(messageId, user,) params,
return NextResponse.json(data) }: {
params: Promise<{ messageId: string }>;
}
) {
const { messageId } = await params;
const { user } = getInfo(request);
const { data }: any = await client.getSuggested(messageId, user);
return NextResponse.json(data);
} }

View File

@ -1,20 +1,30 @@
import { type NextRequest } from 'next/server' import { type NextRequest } from "next/server";
import { ChatClient } from 'dify-client-plus' import { ChatClient } from "dify-client-plus";
import { v4 } from 'uuid' import { v4 } from "uuid";
import { API_KEY, API_URL } from '@/config' import { API_KEY, API_URL } from "@/config";
export const getInfo = (request: NextRequest) => { export const getInfo = (request: NextRequest) => {
const username = request.cookies.get('username')?.value || 'no-user' const username = request.cookies.get("username")?.value || "no-user";
const sessionId = request.cookies.get('session_id')?.value || v4() const sessionId = request.cookies.get("session_id")?.value || v4();
const user = `${username}` const user = `${username}`;
return { return {
sessionId, sessionId,
user, user,
} };
} };
export const setSession = (sessionId: string) => { export const setSession = (sessionId: string) => {
return { 'Set-Cookie': `session_id=${sessionId}` } return { "Set-Cookie": `session_id=${sessionId}` };
} };
export const client = new ChatClient(API_KEY, API_URL || undefined) export const client = new ChatClient(API_KEY, API_URL || undefined);
export function getCookieValue(cookieName: string): string | null {
const cookies = document.cookie.split("; ").reduce((acc, cookie) => {
const [name, value] = cookie.split("=");
acc[name] = value;
return acc;
}, {} as Record<string, string>);
return cookies[cookieName] || null;
}

View File

@ -11,7 +11,7 @@ import { ToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
// import { fetchAgentLogDetail } from '@/service/log' // import { fetchAgentLogDetail } from '@/service/log'
// TODO // TODO
const fetchAgentLogDetail = () => { const fetchAgentLogDetail: any = (data: any) => {
console.log('TODO MARS') console.log('TODO MARS')
} }

View File

@ -39,8 +39,7 @@ const AnswerIcon: FC<AnswerIconProps> = ({
> >
{isValidImageIcon {isValidImageIcon
? <img src={imageUrl} className="w-full h-full rounded-full" alt="answer icon" /> ? <img src={imageUrl} className="w-full h-full rounded-full" alt="answer icon" />
: (icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' /> : (icon && icon !== '') ? (<em-emoji id={icon} />) : (<em-emoji id='🤖' />) }
}
</div> </div>
} }

View File

@ -4,7 +4,8 @@ import { t } from 'i18next'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import s from './style.module.css' import s from './style.module.css'
import Tooltip from '@/app/components/base/tooltip' // import Tooltip from '@/app/components/base/tooltip'
import { Tooltip } from 'antd';
type ICopyBtnProps = { type ICopyBtnProps = {
value: string value: string
@ -31,8 +32,7 @@ const CopyBtn = ({
return ( return (
<div className={`${className}`}> <div className={`${className}`}>
<Tooltip <Tooltip
popupContent={(isCopied ? t('appApi.copied') : t('appApi.copy'))} title={(isCopied ? t('appApi.copied') : t('appApi.copy'))}
asChild={false}
> >
<div <div
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}

View File

@ -1,7 +1,8 @@
'use client' 'use client'
import { t } from 'i18next' import { t } from 'i18next'
import { Refresh } from '@/app/components/base/icons/src/vender/line/general' import { Refresh } from '@/app/components/base/icons/src/vender/line/general'
import Tooltip from '@/app/components/base/tooltip' // import Tooltip from '@/app/components/base/tooltip'
import { Tooltip } from 'antd';
type Props = { type Props = {
className?: string className?: string
@ -12,7 +13,7 @@ const RegenerateBtn = ({ className, onClick }: Props) => {
return ( return (
<div className={`${className}`}> <div className={`${className}`}>
<Tooltip <Tooltip
popupContent={t('appApi.regenerate') as string} title={t('appApi.regenerate') as string}
> >
<div <div
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'} className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}

View File

@ -1,34 +1,52 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Modal, Checkbox, Input, message } from 'antd'; import { Modal, Checkbox, Input, message } from 'antd';
import { useTranslation } from 'react-i18next'
const { TextArea } = Input; const { TextArea } = Input;
interface FeedbackModalProps { interface FeedbackModalProps {
open: boolean; open: boolean;
onOk: (selectedOption: number | null, feedbackText: string) => void; onOk: (selectedOption: number | null, feedbackText: string) => Promise<void>;
onCancel: () => void; onCancel: () => void;
} }
const FeedbackModal: React.FC<FeedbackModalProps> = ({ open, onOk, onCancel }) => { const FeedbackModal: React.FC<FeedbackModalProps> = ({ open, onOk, onCancel }) => {
const [selectedOption, setSelectedOption] = useState<number | null>(null); const [selectedOption, setSelectedOption] = useState<number | null>(null);
const [feedbackText, setFeedbackText] = useState<string>(''); const [feedbackText, setFeedbackText] = useState<string>('');
const { t } = useTranslation()
const [confirmLoading, setConfirmLoading] = useState(false);
const handleOk = () => { useEffect(() => {
if (selectedOption === null || !feedbackText) { if (open) {
message.warning('请选择操作类型并填写反馈建议'); setFeedbackText('');
}
}, [open]);
const handleOk = async () => {
if (!feedbackText) {
message.warning('请填写反馈建议');
return; return;
} }
onOk(selectedOption, feedbackText); setConfirmLoading(true)
try {
await onOk(selectedOption, feedbackText);
} finally {
setConfirmLoading(false)
}
}; };
return ( return (
<Modal <Modal
title="反馈" title="反馈"
open={open} open={open}
onOk={handleOk} onOk={handleOk}
onCancel={onCancel} onCancel={onCancel}
okText={`${t('common.operation.confirm')}`}
cancelText={`${t('common.operation.cancel')}`}
confirmLoading={confirmLoading}
> >
<div> {/* <div>
<Checkbox <Checkbox
checked={selectedOption === 0} checked={selectedOption === 0}
onChange={() => setSelectedOption(0)} onChange={() => setSelectedOption(0)}
@ -41,7 +59,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ open, onOk, onCancel }) =
> >
</Checkbox> </Checkbox>
</div> </div> */}
<TextArea <TextArea
rows={4} rows={4}
placeholder="请输入您的反馈建议" placeholder="请输入您的反馈建议"
@ -52,4 +70,4 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ open, onOk, onCancel }) =
); );
}; };
export default FeedbackModal; export default FeedbackModal;

View File

@ -46,7 +46,7 @@ export type ChatWithHistoryContextValue = {
isMobile: boolean isMobile: boolean
isInstalledApp: boolean isInstalledApp: boolean
appId?: string appId?: string
handleFeedback: (messageId: string, feedback: Feedback) => void handleFeedback: (messageId: string, feedback: Feedback) => Promise<boolean>
currentChatInstanceRef: RefObject<{ handleStop: () => void }> currentChatInstanceRef: RefObject<{ handleStop: () => void }>
themeBuilder?: ThemeBuilder themeBuilder?: ThemeBuilder
} }
@ -73,7 +73,7 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
chatShouldReloadKey: '', chatShouldReloadKey: '',
isMobile: false, isMobile: false,
isInstalledApp: false, isInstalledApp: false,
handleFeedback: () => { }, handleFeedback: () => new Promise(() => { }),
currentChatInstanceRef: { current: { handleStop: () => { } } }, currentChatInstanceRef: { current: { handleStop: () => { } } },
}) })
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext) export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

View File

@ -4,7 +4,7 @@ import useSWR from "swr";
import { useLocalStorageState } from "ahooks"; import { useLocalStorageState } from "ahooks";
import { produce } from "immer"; import { produce } from "immer";
import { message } from "antd"; import { message } from "antd";
import { client, getInfo } from "@/app/api/utils/common"; import { client, getCookieValue } from "@/app/api/utils/common";
import type { Callback, ChatConfig, ChatItem, Feedback } from "../types"; import type { Callback, ChatConfig, ChatItem, Feedback } from "../types";
import { CONVERSATION_ID_INFO } from "../constants"; import { CONVERSATION_ID_INFO } from "../constants";
import { buildChatItemTree } from "../utils"; import { buildChatItemTree } from "../utils";
@ -537,16 +537,43 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setCurrentConversationIdForFeedback, setCurrentConversationIdForFeedback,
] = useState<string | null>(null); ] = useState<string | null>(null);
const [resolveFeedback, setResolveFeedback] = useState<(value: boolean | PromiseLike<boolean>) => void | null>(null);
const [isSubmittingNow, setIsSubmittingNow] = useState(false); // 防止重复提交
const handleFeedback = useCallback( const handleFeedback = useCallback(
(messageId: string, feedback: Feedback, conversationId: string) => { async (messageId: string, feedback: Feedback, conversationId: string) => {
console.log('feedback.rating ', feedback.rating);
setCurrentMessageId(messageId); setCurrentMessageId(messageId);
setCurrentFeedback(feedback); setCurrentFeedback(feedback);
setCurrentConversationIdForFeedback(conversationId); setCurrentConversationIdForFeedback(conversationId);
setIsFeedbackModalVisible(true);
if (feedback.rating === 'like') {
setIsSubmittingNow(true)
return true
} else if (!feedback.rating) {
setIsSubmittingNow(true)
return true
} else
{
return new Promise<boolean>(async (resolve) => {
console.log('handleFeedback', messageId, feedback, conversationId);
setIsFeedbackModalVisible(true);
setResolveFeedback(() => resolve);
})
}
}, },
[] []
); );
useEffect(() => {
if (isSubmittingNow) {
handleFeedbackSubmit(null, '')
}
}, [])
const handleFeedbackSubmit = useCallback( const handleFeedbackSubmit = useCallback(
async (selectedOption: number | null, feedbackText: string) => { async (selectedOption: number | null, feedbackText: string) => {
console.log(currentMessageId); console.log(currentMessageId);
@ -555,16 +582,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
try { try {
// 构造请求参数 // 构造请求参数
const requestBody = { const requestBody = {
operationType: selectedOption, // 0 或 1 operationType: currentFeedback!.rating, // 0 或 1
feedbackText, // 用户反馈建议 feedbackText, // 用户反馈建议
conversationId: '89fba8a7-e4e2-4167-8b7c-b3c103797053', // 会话 ID conversationId: currentConversationId, // 会话 ID
messageId: currentMessageId, // 消息 ID messageId: currentMessageId, // 消息 ID
username: "user_admin", // 用户名 username: getCookieValue('username'), // 用户名
}; };
// 调用 Java 接口 // 调用 Java 接口
const javaResponse = await fetch( const javaResponse = await fetch(
`${process.env.NEXT_PUBLIC_BASE_API_URL}/api/conversation/feedback`, `/dev-api/api/conversation/feedback`,
{ {
method: "POST", method: "POST",
headers: { headers: {
@ -578,26 +605,33 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
await updateFeedback( await updateFeedback(
{ {
url: `/messages/${currentMessageId}/feedbacks`, url: `/messages/${currentMessageId}/feedbacks`,
body: { rating: currentFeedback.rating }, body: { rating: currentFeedback!.rating },
}, },
isInstalledApp, // isInstalledApp,
appId // appId
); );
// 显示成功通知 // 显示成功通知
notify({ type: "success", message: t("common.api.success") }); notify({ type: "success", message: t("common.api.success") });
if (resolveFeedback) {
resolveFeedback(true); // 用户取消了反馈
setResolveFeedback(null);
}
// 关闭对话框 // 关闭对话框
setIsFeedbackModalVisible(false); setIsFeedbackModalVisible(false);
} catch (error) { } catch (error) {
console.error("Error:", error); console.error("Error:", error);
notify({ type: "error", message: t("common.api.failed") }); notify({ type: "error", message: t("common.api.failed") });
} finally {
setIsSubmittingNow(false);
} }
}, },
[ [
currentMessageId, currentMessageId,
currentFeedback, currentFeedback,
currentConversationIdForFeedback, currentConversationId,
isInstalledApp, isInstalledApp,
appId, appId,
notify, notify,
@ -649,7 +683,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
<FeedbackModal <FeedbackModal
open={isFeedbackModalVisible} open={isFeedbackModalVisible}
onOk={handleFeedbackSubmit} onOk={handleFeedbackSubmit}
onCancel={() => setIsFeedbackModalVisible(false)} onCancel={() => {
if (resolveFeedback) {
resolveFeedback(false); // 用户取消了反馈
setResolveFeedback(null);
}
setIsFeedbackModalVisible(false)
}}
/> />
), ),
}; };

View File

@ -199,7 +199,7 @@ const ChatWithHistoryWrapWithCheckToken: FC<ChatWithHistoryWrapProps> = ({
if (!accessToken) { if (!accessToken) {
router.replace("/login"); router.replace("/login");
} else { } else {
fetch(`${process.env.NEXT_PUBLIC_BASE_API_URL}/getInfo`, { fetch(`/dev-api/getInfo`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,

View File

@ -3,6 +3,7 @@ import {
memo, memo,
useMemo, useMemo,
useState, useState,
useCallback
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
@ -19,8 +20,9 @@ import {
ThumbsDown, ThumbsDown,
ThumbsUp, ThumbsUp,
} from '@/app/components/base/icons/src/vender/line/alertsAndFeedback' } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import Tooltip from '@/app/components/base/tooltip' // import Tooltip from '@/app/components/base/tooltip'
import Log from '@/app/components/chat/log' import Log from '@/app/components/chat/log'
import { message, Popconfirm, Tooltip } from 'antd';
interface OperationProps { interface OperationProps {
item: ChatItem item: ChatItem
@ -75,10 +77,19 @@ const Operation: FC<OperationProps> = ({
if (!config?.supportFeedback || !onFeedback) if (!config?.supportFeedback || !onFeedback)
return return
await onFeedback?.(id, { rating }) if (await onFeedback?.(id, { rating })) {
setLocalFeedback({ rating }) setLocalFeedback({ rating })
}
} }
const confirmCancelCallback = useCallback(async () => {
try {
await handleFeedback(null)
} finally {
message.success(localFeedback!.rating === 'like' ? `${t('appDebug.operation.cancelAgree')}成功` : `${t('appDebug.operation.cancelDisagree')}成功`)
}
}, [handleFeedback])
const operationWidth = useMemo(() => { const operationWidth = useMemo(() => {
let width = 0 let width = 0
if (!isOpeningStatement) if (!isOpeningStatement)
@ -138,7 +149,7 @@ const Operation: FC<OperationProps> = ({
)} )}
</div> </div>
)} )}
{/* {/*
{(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && ( {(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
<AnnotationCtrlBtn <AnnotationCtrlBtn
appId={config?.appId || ''} appId={config?.appId || ''}
@ -170,7 +181,7 @@ const Operation: FC<OperationProps> = ({
{ {
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && ( config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
<div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'> <div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
<Tooltip popupContent={t('appDebug.operation.agree')}> <Tooltip title={t('appDebug.operation.agree')}>
<div <div
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer' className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
onClick={() => handleFeedback('like')} onClick={() => handleFeedback('like')}
@ -179,7 +190,7 @@ const Operation: FC<OperationProps> = ({
</div> </div>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
popupContent={t('appDebug.operation.disagree')} title={t('appDebug.operation.disagree')}
> >
<div <div
className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer' className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
@ -194,27 +205,37 @@ const Operation: FC<OperationProps> = ({
{ {
config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && ( config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
<Tooltip <Tooltip
popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')} title={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}
> >
<div <Popconfirm
className={` title={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}
flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer description={localFeedback.rating === 'like' ? `是否${t('appDebug.operation.cancelAgree')}` : `是否${t('appDebug.operation.cancelDisagree')}`}
${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'} onConfirm={confirmCancelCallback}
${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'} okText={`${t('common.operation.confirm')}`}
`} cancelText={`${t('common.operation.cancel')}`}
onClick={() => handleFeedback(null)}
> >
{ <button
localFeedback.rating === 'like' && ( type="button"
<ThumbsUp className='w-4 h-4' /> className={`
) flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
} ${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
{ ${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
localFeedback.rating === 'dislike' && ( `}
<ThumbsDown className='w-4 h-4' /> onClick={(e) => e.stopPropagation()}
) >
} {
</div> localFeedback.rating === 'like' && (
<ThumbsUp className='w-4 h-4' />
)
}
{
localFeedback.rating === 'dislike' && (
<ThumbsDown className='w-4 h-4' />
)
}
</button>
</Popconfirm>
</Tooltip> </Tooltip>
) )
} }

View File

@ -60,7 +60,7 @@ export type ChatProps = {
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
onAnnotationRemoved?: (index: number) => void onAnnotationRemoved?: (index: number) => void
chatNode?: ReactNode chatNode?: ReactNode
onFeedback?: (messageId: string, feedback: Feedback) => void onFeedback?: (messageId: string, feedback: Feedback) => Promise<boolean>
chatAnswerContainerInner?: string chatAnswerContainerInner?: string
hideProcessDetail?: boolean hideProcessDetail?: boolean
hideLogModal?: boolean hideLogModal?: boolean

View File

@ -1,70 +1,70 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
export default function LoginPage() { export default function LoginPage() {
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const router = useRouter() const router = useRouter()
const [loading, setLoading] = useState(false) // 新增加载状态 const [loading, setLoading] = useState(false) // 新增加载状态
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setLoading(true) // 开始加载 setLoading(true) // 开始加载
fetch(`${process.env.NEXT_PUBLIC_BASE_API_URL}/login`, { fetch(`/dev-api/login`, {
method: 'post', method: 'post',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password }),
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
if (data.code === 200) { if (data.code === 200) {
localStorage.setItem('token', data.token) localStorage.setItem('token', data.token)
document.cookie = `username=${username}; path=/` // 新增cookie设置 document.cookie = `username=${username}; path=/` // 新增cookie设置
router.push('/') router.push('/')
} else { } else {
setError('登录失败,请检查凭证') setError('登录失败,请检查凭证')
setLoading(false) setLoading(false)
} }
}) })
} }
return ( return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center"> <div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg shadow-md w-96"> <div className="bg-white p-8 rounded-lg shadow-md w-96">
<h1 className="text-2xl font-bold mb-6 text-center">Login</h1> <h1 className="text-2xl font-bold mb-6 text-center">Login</h1>
<form onSubmit={handleLogin}> <form onSubmit={handleLogin}>
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium mb-1">Username</label> <label className="block text-sm font-medium mb-1">Username</label>
<input <input
type="text" type="text"
className="w-full px-3 py-2 border rounded-md" className="w-full px-3 py-2 border rounded-md"
value={username} value={username}
onChange={e => setUsername(e.target.value)} onChange={e => setUsername(e.target.value)}
required required
/> />
</div> </div>
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium mb-1">Password</label> <label className="block text-sm font-medium mb-1">Password</label>
<input <input
type="password" type="password"
className="w-full px-3 py-2 border rounded-md" className="w-full px-3 py-2 border rounded-md"
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
required required
/> />
</div> </div>
{error && <p className="text-red-500 text-sm mb-4">{error}</p>} {error && <p className="text-red-500 text-sm mb-4">{error}</p>}
<button <button
type="submit" type="submit"
disabled={loading} // 禁用按钮 disabled={loading} // 禁用按钮
className={`w-full bg-blue-500 text-white py-2 px-4 rounded-md transition-colors ${!loading && 'hover:bg-blue-600'}`} className={`w-full bg-blue-500 text-white py-2 px-4 rounded-md transition-colors ${!loading && 'hover:bg-blue-600'}`}
> >
{loading ? 'Loading...' : 'Sign In'} {loading ? 'Loading...' : 'Sign In'}
</button> </button>
</form> </form>
</div> </div>
</div> </div>
) )
} }

View File

@ -4,6 +4,7 @@ import React from 'react'
import type { IMainProps } from '@/app/components' import type { IMainProps } from '@/app/components'
import ChatWithHistoryWrapWithCheckToken from '@/app/components/chat-with-history' import ChatWithHistoryWrapWithCheckToken from '@/app/components/chat-with-history'
import '@ant-design/v5-patch-for-react-19';
const App: FC<IMainProps> = ({ const App: FC<IMainProps> = ({
params, params,

View File

@ -1,11 +1,13 @@
@import "preflight.css"; @import "preflight.css";
@tailwind base;
@tailwind components;
@import '../../themes/light.css'; @import '../../themes/light.css';
@import '../../themes/dark.css'; @import '../../themes/dark.css';
@import "../../themes/manual-light.css"; @import "../../themes/manual-light.css";
@import "../../themes/manual-dark.css"; @import "../../themes/manual-dark.css";
@import "../components/base/button/index.css";
@import "../components/base/action-button/index.css";
@import "../components/base/modal/index.css";
@tailwind base;
@tailwind components;
html { html {
color-scheme: light; color-scheme: light;
@ -680,8 +682,4 @@ button:focus-within {
display: none; display: none;
} }
@import "../components/base/button/index.css"; @tailwind utilities;
@import "../components/base/action-button/index.css";
@import "../components/base/modal/index.css";
@tailwind utilities;

32
eslint.config.js Normal file
View File

@ -0,0 +1,32 @@
const antfu = require('@antfu/eslint-config').default
// module.exports = antfu()
export default {
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',
},
}

View File

@ -1,32 +1,44 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
eslint: {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
},
typescript: {
// !! WARN !!
// Dangerously allow production builds to successfully complete even if
// your project has type errors.
// !! WARN !!
ignoreBuildErrors: true,
},
reactStrictMode: true, reactStrictMode: true,
// 压缩优化Next.js 13+默认启用SWC无需手动配置 // 压缩优化Next.js 13+默认启用SWC无需手动配置
compress: true, compress: true,
// 图片优化 // 图片优化
images: { images: {
formats: ["image/avif", "image/webp"], formats: ['image/avif', 'image/webp'],
domains: ["cdn.yourdomain.com"], domains: ['cdn.yourdomain.com'],
}, },
// 修正后的开发指示器配置 // 修正后的开发指示器配置
devIndicators: { devIndicators: {
position: "bottom-right", // 新版统一用position position: 'bottom-right', // 新版统一用position
}, },
// 页面扩展名 // 页面扩展名
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
// 路由重写 // 路由重写
async rewrites() { async rewrites() {
return [ return [
{ {
source: "/dev-api/:path*", source: '/dev-api/:path*',
destination: "http://192.168.6.9:8085/:path*", destination: `${process.env.NEXT_PUBLIC_BASE_API_URL}/:path*`,
}, },
]; ]
}, },
}; }
module.exports = nextConfig; module.exports = nextConfig

View File

@ -37,6 +37,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@emoji-mart/data": "^1.2.1", "@emoji-mart/data": "^1.2.1",
"@floating-ui/react": "^0.27.3", "@floating-ui/react": "^0.27.3",
"@formatjs/intl-localematcher": "^0.5.10", "@formatjs/intl-localematcher": "^0.5.10",
@ -51,6 +52,7 @@
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"antd": "^5.26.4",
"axios": "^1.7.9", "axios": "^1.7.9",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",

10497
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff