Vite react 环境搭建
1. 安装 vite 脚手架
npm create vite@latest
选择: react
Typescript
2 安装扣子需要的环境
# 安装扣子
npm install @coze/api
# Install axios
npm install axios
# ant design
npm install antd
npm install @ant-design/x
npm install antd-style
3 示例
4 Chat 组件 代码 (基础)
将下面的代码放到同一个目录,把 放到组件里
index.tsx
import React, { useState } from 'react';
import './index.css';
import { useCozeAPI } from './use-coze-api';
import Setting, { type SettingConfig } from './setting';
function Chat() {
const { initClient, message, sendMessage, isReady, uploadFile } =
useCozeAPI();
const [isModify, setIsModify] = useState(false);
const [query, setQuery] = useState('');
const [file, setFile] = useState<File | null>(null);
const handleSubmit = (settingConfig: SettingConfig) => {
initClient(settingConfig);
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
setFile(event.target.files[0]);
}
};
const handleUpload = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
if (file) {
uploadFile(file);
}
} catch (error) {
console.error(error);
}
};
return (
<div className="App">
<Setting onSubmit={handleSubmit} onChange={setIsModify} />
<div className="header">
<div>
<form onSubmit={handleUpload}>
<input type="file" onChange={handleFileChange} name="file" />
<button type="submit" disabled={isModify || !isReady}>
Upload
</button>
</form>
</div>
<div>
<input
value={query}
onChange={e => {
setQuery(e.target.value);
}}
placeholder={`It's ${isReady && !isModify ? 'ready' : 'not ready'}`}
></input>
<button
disabled={isModify || !isReady}
onClick={() => {
sendMessage(query);
setQuery('');
}}
>
send message
</button>
</div>
</div>
<pre className="content">
<code>{message}</code>
</pre>
</div>
);
}
export default Chat;
index.css
.auth {
width: 612px;
margin: 0 auto;
.item {
margin-top: 10px;
width: 800px;
}
input,
select {
width: 500px;
height: 24px;
}
button {
margin-top: 10px;
width: 80px;
height: 30px;
}
}
.header {
margin-top: 10px;
align-items: center;
width: 100%;
input {
width: 480px;
height: 30px;
border: 1px solid black;
border-radius: 5px;
}
button {
margin-left: 10px;
width: 120px;
height: 30px;
border: 1px solid black;
border-radius: 5px;
}
> div {
margin: 10px auto;
width: 620px;
}
}
.content {
/** 实现内容垂直居中 **/
width: 612px;
height: 400px;
overflow-y: auto;
border: 1px solid black;
margin: 20px auto;
display: flex;
justify-content: center;
white-space: pre-wrap;
}
setting.tsx
import { useEffect, useState } from 'react';
import './index.css';
export interface SettingConfig {
authType: string;
baseUrl: string;
token?: string;
botId: string;
clientId?: string;
clientSecret?: string;
}
interface Props {
onSubmit: (settingConfig: SettingConfig) => void;
onChange: (isModify: boolean) => void;
}
function Setting({ onSubmit, onChange }: Props) {
const [botId, setBotId] = useState('');
const [token, setToken] = useState('');
const [authType, setAuthType] = useState('pat_token');
const [clientId, setClientId] = useState('');
const [clientSecret, setClientSecret] = useState('');
const [baseUrl, setBaseUrl] = useState('https://api.coze.cn');
const [isModify, setIsModify] = useState(false);
useEffect(() => {
const settingConfig = localStorage.getItem('settingConfig');
if (settingConfig) {
const config = JSON.parse(settingConfig);
setAuthType(config.authType);
setBotId(config.botId);
setToken(config.token);
setClientId(config.clientId);
setClientSecret(config.clientSecret);
setBaseUrl(config.baseUrl);
}
}, []);
useEffect(() => {
onChange(isModify);
}, [isModify]);
const handleSubmit = () => {
const settingConfig = {
authType,
token: authType === 'pat_token' ? token : '',
botId,
clientId,
clientSecret,
baseUrl,
};
onSubmit(settingConfig);
setIsModify(false);
onChange(false);
localStorage.setItem('settingConfig', JSON.stringify(settingConfig));
};
return (
<div className="auth">
<div className="item">
<label>Auth Type:</label>
<select
value={authType}
onChange={e => {
setAuthType(e.target.value);
setToken('');
setClientId('');
setIsModify(true);
}}
>
<option value="pat_token">pat_token</option>
<option value="oauth_token">oauth_token</option>
<option value="oauth_pkce">oauth_pkce</option>
</select>
</div>
<div className="item">
<label>Base URL:</label>
<input
value={baseUrl}
onChange={e => {
setBaseUrl(e.target.value);
setIsModify(true);
}}
></input>
</div>
<div className="item">
<label>Bot Id:</label>
<input
value={botId}
onChange={e => {
setBotId(e.target.value);
setIsModify(true);
}}
></input>
</div>
{authType === 'pat_token' && (
<div className="item">
<label>Pat Token:</label>
<input
value={token}
onChange={e => {
setToken(e.target.value);
setIsModify(true);
}}
></input>
</div>
)}
{authType !== 'pat_token' && (
<div className="item">
<label>Client Id:</label>
<input
value={clientId}
onChange={e => {
setClientId(e.target.value);
setIsModify(true);
}}
></input>
</div>
)}
{authType === 'oauth_token' && (
<div className="item">
<label>Client Secret:</label>
<input
value={clientSecret}
onChange={e => {
setClientSecret(e.target.value);
setIsModify(true);
}}
></input>
</div>
)}
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
export default Setting;
use-coze-api.ts
import { useEffect, useState } from 'react';
import {
CozeAPI,
getWebOAuthToken,
getPKCEOAuthToken,
getWebAuthenticationUrl,
getPKCEAuthenticationUrl,
type OAuthToken,
} from '@coze/api';
import { type SettingConfig } from './setting';
let client: CozeAPI;
const redirectUrl = 'http://localhost:3000';
// eslint-disable-next-line max-lines-per-function
const useCozeAPI = () => {
const [message, setMessage] = useState('');
const [isReady, setIsReady] = useState(false);
const [fileId, setFileId] = useState('');
const [config, setConfig] = useState<SettingConfig>({
authType: 'pat_token',
token: '',
botId: '',
clientId: '',
clientSecret: '',
baseUrl: '',
});
useEffect(() => {
const configData = JSON.parse(
sessionStorage.getItem('settingConfig') || '{}',
) as SettingConfig;
if (configData && configData.authType) {
if (config.authType === 'pat_token') {
initClient(config);
} else if (
config.authType === 'oauth_token' ||
config.authType === 'oauth_pkce'
) {
if (config.token) {
initClient(config);
return;
}
// get code from url
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (code) {
const codeVerifier = sessionStorage.getItem('codeVerifier') || '';
let result: Promise<OAuthToken>;
if (config.authType === 'oauth_token') {
result = getWebOAuthToken({
baseURL: config.baseUrl,
code,
clientId: config.clientId || '',
redirectUrl,
clientSecret: config.clientSecret || '',
});
} else {
result = getPKCEOAuthToken({
baseURL: config.baseUrl,
code,
clientId: config.clientId || '',
redirectUrl,
codeVerifier,
});
}
result
.then(res => {
config.token = res.access_token;
sessionStorage.setItem('settingConfig', JSON.stringify(config));
initClient(config);
})
.finally(() => {
params.delete('code');
window.history.replaceState(
{},
'',
`${window.location.pathname}?${params.toString()}`,
);
});
}
}
}
}, []);
async function initClient(configData: SettingConfig) {
setConfig(configData);
if (configData.authType === 'oauth_token' && !configData.token) {
window.location.href = getWebAuthenticationUrl({
baseURL: configData.baseUrl,
clientId: configData.clientId || '',
redirectUrl,
state: '',
});
return;
}
if (configData.authType === 'oauth_pkce' && !configData.token) {
const { url, codeVerifier } = await getPKCEAuthenticationUrl({
baseURL: configData.baseUrl,
clientId: configData.clientId || '',
redirectUrl,
state: '',
});
sessionStorage.setItem('codeVerifier', codeVerifier);
window.location.href = url;
return;
}
if (!configData.token) {
return;
}
client = new CozeAPI({
token: configData.token || '',
baseURL: configData.baseUrl,
allowPersonalAccessTokenInBrowser: true,
});
setIsReady(true);
}
async function streamingChat(query: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let messages: any[];
if (fileId) {
messages = [
{
role: 'user',
content: [
{ type: 'file', file_id: fileId },
{ type: 'text', text: query },
],
content_type: 'object_string',
},
];
} else {
messages = [
{
role: 'user',
content: query,
content_type: 'text',
},
];
}
const v = await client.chat.stream({
bot_id: config.botId,
user_id: '1234567890',
auto_save_history: true,
additional_messages: messages,
});
let msg = '';
for await (const part of v) {
if (typeof part === 'string') {
continue;
}
if (part.event === 'conversation.chat.created') {
console.log('[conversation.chat.created] ---> [START]');
} else if (part.event === 'conversation.message.delta') {
msg += part.data.content;
setMessage(msg);
console.log(' [conversation.message.delta]--content-->',part.data.content);
console.log(' [conversation.message.delta]--msg-->',msg);
} else if (part.event === 'conversation.message.completed') {
const { role, type, content } = part.data;
if (role === 'assistant' && type === 'answer') {
msg += '\n';
setMessage(msg);
console.log('[conversation.message.completed] ----> [%s]:[%s]:%s', role, type, content);
} else {
console.log('[conversation.message.completed] ----> [%s]:[%s]:%s', role, type, content);
if (type === 'function_call') {
try {
const functionCall = JSON.parse(content);
console.log('---function_call-->', functionCall['plugin_name'])
console.log('[conversation.message.completed] 【function】 ----> [%s]:[%s]:%s', role, type, JSON.stringify(functionCall, null, 2));
} catch (e) {
console.error('Failed to parse function call content:', e);
console.log('[conversation.message.completed] 【function】 ----> [%s]:[%s]:%s', role, type, content);
}
}else if (type === 'tool_response') {
try {
console.log('---tool_response-->', content)
} catch (e) {
console.error('Failed to parse function call content:', e);
console.log('[conversation.message.completed] 【function】 ----> [%s]:[%s]:%s', role, type, content);
}
}
}
} else if (part.event === 'conversation.chat.completed') {
console.log('[conversation.chat.completed] -->',part.data.usage);
} else if (part.event === 'done') {
console.log('[done]',part.data);
}
}
console.log('=== End of Streaming Chat ===');
}
const sendMessage = async (query: string) => {
setMessage('');
await streamingChat(query);
};
const uploadFile = async (file: File) => {
setIsReady(false);
try {
const res = await client.files.upload({ file });
setFileId(res.id);
setIsReady(true);
console.log(res);
} catch (e) {
console.error(e);
}
};
return { message, sendMessage, initClient, isReady, uploadFile };
};
export { useCozeAPI };