项目背景
利用LLM的prompt做了个简单的服务推荐助手,依据用户的提问找出合适的服务项目推荐给的用户。为了测评prompt的效果,使用react+chainlit开发了一个简单的效果测评平台。在该平台上,可以模拟用户向LLM发出问题,并对大模型返回的服务项目进行评判。其最终呈现结果如下:
鉴于后端prompt暂时无法公开,这里仅公开前端的react代码,至于后端代码可以参考chainlit官方提供的样例(https://github.com/Chainlit/cookbook/tree/main/custom-frontend)。
文件1:src/components/Page.tsx
import "./Page.css";
import {
useChatInteract,
IStep,
useChatMessages
} from "@chainlit/react-client";
import { useState} from "react";
import {BiSolidDislike,BiDislike } from "react-icons/bi";
function Page() {
const [inputValue, setInputValue] = useState("");
const { sendMessage } = useChatInteract();
const { messages } = useChatMessages();
const [multiClickStates,setMultiClickStates]=useState([Array(5).fill(false)]);
const [disableStates,setDisableStates]=useState([false]);
const handleSendMessage = () => {
const content = inputValue.trim();
if (content) {
const message = {
name: "User",
type: "user_message" as const,
output: content,
};
sendMessage(message, []);
setInputValue("");
}
};
const handleSubmit=(index:number,length:number)=>{
if (index===disableStates.length-1){
setDisableStates((prevDisableStates)=>{
const newDisableStates=[...prevDisableStates];
newDisableStates[newDisableStates.length-1]=true;
newDisableStates.push(false);
return newDisableStates;
});
setMultiClickStates((prevClickStates)=>{
const newMultiClickStates = [...prevClickStates];
const clickLength=newMultiClickStates.length-1;
newMultiClickStates[clickLength]=newMultiClickStates[clickLength].slice(0,length);
newMultiClickStates.push(Array(5).fill(false));
return newMultiClickStates;
});
}
};
const handleClick = (index:number) => {
setMultiClickStates((prevClickStates)=>{
const clickLength=prevClickStates.length-1;
const newMultiClickStates = [...prevClickStates];
newMultiClickStates[clickLength]=[...newMultiClickStates[clickLength]];
newMultiClickStates[clickLength][index]=!newMultiClickStates[clickLength][index];
return newMultiClickStates;
});
};
const BotMessage = (output: string,idx:number) => {
const result=JSON.parse(output);
if (result.flag==="F"){
return (
<div>
<div className="chat-message">{result.content}</div>
{idx!==-1 && <button onClick={()=>handleSubmit(idx,0)} className="submit-button">
{disableStates[idx]?<span>已提交</span>:<span>提交(必选)</span>}
</button>}
</div>
)
} else {
return (
<div>
<div className="chat-message">好的,根据您的问题我向您推荐以下服务:</div>
{result.content.map((item:string,index:number)=>{
return (
<div className="chat-service-item" key={index}>
<span>{item}</span>
<div className="dislike-icon" onClick={()=>handleClick(index)}>
{multiClickStates[idx][index]?<BiSolidDislike size={24} className="dislike-clicked"/>:<BiDislike size={24} className="dislike-unclick"/>}
</div>
</div>
)})
}
<button onClick={()=>handleSubmit(idx,result.content.length)} className="submit-button">
{disableStates[idx]?<span>已提交</span>:<span>提交(必选)</span>}
</button>
</div>
)
}
};
const renderMessage = (message: IStep,index:number) => {
const dateOptions: Intl.DateTimeFormatOptions = {
hour: "2-digit",
minute: "2-digit",
};
const date = new Date(message.createdAt).toLocaleTimeString(
undefined,
dateOptions
);
if(message.type === "user_message") {
return (
<div key={message.id} className="chat-box-user">
<div className="user-avatar">U</div>
<div className="bot-user-content">
<div className="user-icon">
<div className="bot-user-name">{message.name}</div>
<div className="bot-user-time">{date}</div>
</div>
<div className="user-chat-message">{message.output}</div>
</div>
</div>
);
} else {
return (
<div key={message.id} className="chat-box-bot">
<div className="bot-avatar">B</div>
<div className="bot-user-content">
<div className="bot-icon">
<div className="bot-user-name">{message.name}</div>
<div className="bot-user-time">{date}</div>
</div>
{BotMessage(message.output,Math.floor(index/2)-1)}
</div>
</div>
);
};
};
return (
<div className="chat-container">
<div className="chat-box">
{messages.map((message,index) => renderMessage(message,index))}
</div>
<div className="fixed-bottom">
<input className="fixed-bottom-input"
type="text"
placeholder="输入你的问题..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyUp={(e) => {
if (e.key === "Enter") {
handleSendMessage();
}
}}>
</input>
<button onClick={handleSendMessage} className="button" type="submit">Send</button>
</div>
</div>
);
}
export default Page;
文件2:src/components/Page.css
.chat-container{
display: flex;
flex-direction: column;
align-items: center; /* 水平方向居中 */
width: 100%;
min-height: 100%;
margin: 0;
padding: 0;
}
.chat-box{
height:550px;
width:70%;
overflow-y:auto;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.chat-box-bot{
align-self: flex-start;
margin-bottom:10px;
}
.chat-box-user{
align-self: flex-end;
margin-bottom:10px;
}
@media (max-width: 991px) {
.chat-box-bot,.chat-box-user {
max-width: 100%;
}
}
.bot-user-content{
overflow:auto;
margin-bottom:5px;
padding-top:5px;
padding-bottom:5px;
}
.user-avatar{
float:right;
width: 40px;
height: 40px;
background-color: #f92672;
display: flex;
justify-content:center;
align-items: center;
border-radius: 50%;
font-size: 24px;
color: white;
margin-left: 10px;
font-weight:bold;
margin-top:5px;
}
.bot-avatar{
float:left;
width: 40px;
height: 40px;
background-color: #f92672;
display: flex;
justify-content:center;
align-items: center;
border-radius: 50%;
font-size: 24px;
color: white;
margin-right: 10px;
font-weight: bold;
margin-top:5px;
}
.bot-icon{
align-content: center;
flex-wrap: wrap;
display: flex;
gap: 12px;
}
.user-icon{
align-content: center;
flex-wrap: wrap;
display: flex;
gap: 12px;
text-align: right;
flex-direction: row-reverse; /* 让子元素从右到左排列 */
justify-content: flex-start;
}
.bot-user-name{
color: #000;
font-feature-settings: "dlig" on;
font: 700 16px Manrope, sans-serif;
}
.bot-user-time{
color: #6b6b6b;
font-feature-settings: "dlig" on;
font: 400 16px/150% Manrope, -apple-system, Roboto, Helvetica,
sans-serif;
}
@media (max-width: 991px) {
.bot-user-time{
max-width: 100%;
}
}
.chat-message {
color: #000;
font-feature-settings: "dlig" on;
margin-top: 4px;
font: 400 18px/24px Manrope, -apple-system, Roboto, Helvetica,
sans-serif;
width:918px;
}
@media (max-width: 991px) {
.chat-message {
max-width: 100%;
}
}
.user-chat-message{
color: #000;
font-feature-settings: "dlig" on;
margin-top: 4px;
font: 400 18px/24px Manrope, -apple-system, Roboto, Helvetica,
sans-serif;
text-align: right;
}
@media (max-width: 991px) {
.user-chat-message {
max-width: 100%;
}
}
.chat-service-item{
background-color:rgb(229, 246, 253);
padding:15px;
height:25px;
border-radius: 12px 12px 12px 12px;
margin: 10px 0px 10px 0px;
font: 400 14px/24px Manrope, -apple-system, Roboto, Helvetica,sans-serif;
color:rgb(1, 67, 97);
position:relative;
width:700px;
line-height: 25px;
}
.dislike-icon{
position:absolute;
margin-top:2px;
top:50%;
right:50px;
transform:translateY(-50%);
}
.dislike-clicked{
color:rgb(1, 67, 97);
}
.dislike-unclicked{
color:rgb(1, 67, 97);
}
.submit-button{
height:45px;
border-radius: 12px;
width:100px;
background-color:rgb(229, 246, 253);
border: 0px;
font: 400 14px/24px Manrope, -apple-system, Roboto, Helvetica,sans-serif;
color:rgb(1, 67, 97);
}
.fixed-bottom {
position: sticky;
padding-top:40px;
bottom: 0;
z-index: 1000;
border-radius: 12px;
display: flex;
width: 100%;
max-width: 850px;
gap: 0px;
line-height: 0%;
}
@media (max-width: 991px) {
.fixed-bottom {
max-width: 100%;
flex-wrap: wrap;
margin-bottom: 40px;
}
}
.fixed-bottom-input {
height:35px;
font-feature-settings: "dlig" on;
align-items: start;
border-radius: 12px 12px 12px 12px;
background-color: #ededed;
color: #6b6b6b;
justify-content: center;
flex: 1;
padding: 8px 8px 8px 8px;
width:85%;
font: 400 16px Manrope, sans-serif;
}
@media (max-width: 991px) {
.fixed-bottom-input {
max-width: 100%;
padding-right: 20px;
}
}
.button {
justify-content: center;
border-radius: 12px 12px 12px 12px;
background-color: #000;
font-family: Manrope, sans-serif;
display: flex;
flex-direction: column;
font-size: 14px;
color: #fff;
font-weight: 500;
white-space: nowrap;
text-align: center;
padding: 8px 8px 8px 8px;
margin-left:10px;
}
@media (max-width: 991px) {
.button {
white-space: initial;
}
}
文件3: App.tsx
import './App.css';
import Page from './components/Page';
import { useEffect } from "react";
import { sessionState, useChatSession } from "@chainlit/react-client";
import { useRecoilValue } from "recoil";
const userEnv = {};
function App() {
const { connect } = useChatSession();
const session = useRecoilValue(sessionState);
useEffect(() => {
if (session?.socket.connected) {
return;
}
fetch("http://localhost:80/custom-auth")
.then((res) => {
return res.json();
})
.then((data) => {
connect({
userEnv,
accessToken: `Bearer: ${data.token}`,
});
});
}, [connect]);
return (
<div className="App">
<div className="App-container">
<div className="title">意图识别效果评测平台</div>
<Page />
</div>
</div>
);
}
export default App;
文件4: App.css
.App {
background-color: #FAFAFA;
display: flex;
justify-content: center;
overflow: hidden;
min-height: 100vh;
}
.App-container{
min-height:100vh;
width:100%;
position: relative;
}
.title {
align-self: start;
color: #000;
font-feature-settings: "dlig" on;
text-align: center;
font: 700 32px Manrope, sans-serif;
margin-bottom: 25px;
margin-top:25px;
}
@media (max-width: 991px) {
.title {
margin-left: 10px;
}
}
文件5: index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { RecoilRoot } from "recoil";
import "./index.css";
import { ChainlitAPI, ChainlitContext } from "@chainlit/react-client";
const CHAINLIT_SERVER = "http://localhost:80/chainlit";
const apiClient = new ChainlitAPI(CHAINLIT_SERVER, "webapp");
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ChainlitContext.Provider value={apiClient}>
<RecoilRoot>
<App />
</RecoilRoot>
</ChainlitContext.Provider>
</React.StrictMode>
);
目前版本还未完成将评测结果返回保存下来的功能,后续会补充上来。