[7][Node.js] BOT 对话 :Chat

扣子专业版扣子
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 示例

picture.image

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 };
0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
大规模高性能计算集群优化实践
随着机器学习的发展,数据量和训练模型都有越来越大的趋势,这对基础设施有了更高的要求,包括硬件、网络架构等。本次分享主要介绍火山引擎支撑大规模高性能计算集群的架构和优化实践。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论