引言

本文主要讲述从获取大模型API开始,到运用到springboot前后端分离项目中的整个流程,若是已经获取了API以及对应的API keys,想要直接在项目中使用,可以直接跳转四、在项目中使用API

一、获取大模型API

1.1、使用在线API

若想使用在线大模型的api,需要先去对应大模型的在线api网站上获取对应的api,以deepseek为例,需要先去它的api开放平台,进入API keys页面,然后创建一个属于自己的api,注意,api仅能在创建时看到和复制,创建之后无法获取,如果创建时忘了保存api,可以删掉再创建一个。

1.2、使用本地API

使用本地api需要先去ollama官网下载ollama,下载完成后在cmd命令行窗口输入“ollama”命令,若有出现信息则说明下载成功。

下载完ollama之后需要拉取对应的本地大模型,以deepseek为例,在ollama官网的搜索栏搜索deepseek,进入对应模型的下载页面,并选择下载对应量级的模型,建议下载8b模型,如果本地配置高,可以自行选择更高量级。在进入对应模型的页面并选择好量级后,复制右上角的命令,并在cmd控制台运行并等待下载完成即可。这里建议再下载一个embedding大模型nomic-embed-text:latest。

使用ollama list命令可看到本地下载的所有大模型。

 二、下载应用开发模型(dify)

若要使用知识库,比较简单的方法便是利用dify进行配置,其它的诸如RAGFlow、FastGPT也是一样的效果。dify的配置教程网上有很多,大家可以自行找一个适合自己的,比如可以参考这篇博客保姆教程篇:手把手教你从零开始本地部署Dify工作流-CSDN博客,只要保证最后dify抛出的api能够被自己项目的运行环境访问到即可,这里就不过多赘述。

博主本人是在windows10环境下配置的ollama,然后用VMware虚拟机安装Ubuntu24.04.2配置的docker,建议大家还是把ollama和docker配置到同一环境下,如果有和博主一样情况的,设置一下主机和虚拟机的端口映射即可。

 三、配置dify信息

3.1、进入dify主页

如果前面的步骤顺利完成,那么打开浏览器,输入dify默认的地址http://localhost:80,然后便可进入dify初始的账号密码设置页面,设置完成便可进入dify的主页, 在此,可点击左侧的创建空白应用创建一个自己的应用。

3.2、配置dify模型信息

先在右上角的头像处进入设置页面

接着选择模型供应商,在此选择所使用api对应的厂家,如使用deepseek的在线api则选择深度求索,如果使用的是ollama本地大模型则使用ollama。

在下载好对应的插件之后,配置对应的API key即可,deepseek的api配置如下

若是使用ollama,则可以进行如下配置。

 embedding的模型配置也是同理。

3.3、配置知识库

在配置完对应的大模型之后,便可以开始构建我们的知识库以及对应的工作流了,在知识库页面选择创建知识库,然后上传自己的知识库文件/选择web站点。

点击下一步,索引方式选择高质量,embedding模型选择我们刚才配置的nomic-embed-text:latest模型 (若是使用在线api,可自行选择对应的模型,若无embedding模型,索引方式选择经济即可),配置完成后选择保存并处理。

 3.4、配置对话助手

在配置完知识库之后,来到工作室,选择创建空白应用。

在空白应用处选择需要的应用类型,这里以聊天助手为例。

在具体设置页面,可通过提示词来设置模型回答的偏好,内容等,在下方知识库模块则可以设置我们刚才配置好的知识库。

3.5、获取api

在设置完应用之后,在右上角发布并获取对应的api。如果不需要自定义前端页面或后端的信息,其实直接点击“嵌入网站”即可,但是本文的主要目的就是讲springboot的前后端分离项目如何调用大模型api,因此统一采用访问api的方式进行开发。

在访问api页面,点击右上角的API密钥来创建密钥,至此,api获取成功,下文的访问方式均采用dify的API进行开发,但也会提到不使用dify配置知识库,仅采用在线大模型API的访问方式。

四、在项目中使用API

想要使用大模型或者dify的api,大部分都是通过访问网页给出的url,在请求头中携带获取到的API keys,然后在POST请求里的body参数中携带对应的信息即可,若是不考虑安全性,直接在前端使用fetch请求即可获取对应的流式数据或者块级数据,但是若不想把API keys暴露在前端,则需要在后端再做一次转发操作。

4.1、后端

后端使用 WebClient 进行请求的发送,在使用前,注意在pom.xml文件中引入相关依赖。

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-webflux</artifactId>
 </dependency>

接着,在实体类中定义API接口所需要的参数类,以dify为例

entity/ChatRequest.java

//ChatRequest.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatRequest {
    private Object inputs;
    private String query;
    private String response_mode;
    private String conversation_id;
    private String user;
    private List<Object> files;
}

然后在controller层添加对应的接口,其中,分为两种情况,一种是AI应用较为常见的流式数据,即请求发送后,响应的数据是持续返回的,可以减少用户的初次等待时间,一种是块级数据,即当AI应用处理完所有信息后再全部返回。

controller/AgentController.java

//AgentController.java

@RestController
@RequestMapping("/agent")
public class AgentController {
    public static final String CHAT_URL = "http://localhost/v1/chat-messages";
    public static final String WORKFLOW_URL = "http://localhost/v1/workflows/run";
    public static final String CHAT_APIKEY = "xxxxxxxxx";//获取的应用API key
    public static final String ADVICE_APIKEY = "xxxxxxxxx";获取的应用API key

    private final WebClient chatClient;
    private final WebClient adviceClient;


    public AgentController() {
        this.chatClient = WebClient.builder()
                .baseUrl(CHAT_URL)
                .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + CHAT_APIKEY)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();

        this.adviceClient = WebClient.builder()
                .baseUrl(WORKFLOW_URL)
                .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + ADVICE_APIKEY)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }

    //接收并返回流式数据
        @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> agentChat(@RequestBody ChatRequest request) {
        //request即为发送给大模型api的请求体,由前端传入,也可自行在后端编辑信息
        return chatClient.post()
                .bodyValue(request)
                .accept(MediaType.TEXT_EVENT_STREAM)
                .retrieve()
                .bodyToFlux(String.class);

        //自定义响应体
//        return chatClient.post()
//                .bodyValue(request)
//                .accept(MediaType.TEXT_EVENT_STREAM)
//                .retrieve()
//                .bodyToFlux(String.class)
//                .map(data->{
//                    return "data: "+data;
//                });
    }

    //接收并返回块级数据
    @PostMapping("/advice")
    public Mono<String> agentAdvice(@RequestBody ChatRequest request) {
        return adviceClient.post()
                .bodyValue(request)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(String.class);
    }
}

 若是直接使用的大模型API,则同理,修改一下url和API key,再将ChatRequest类中的参数修改为对应api需要的参数即可,以deepseek为例。

@Data
public class ChatRequest {
    private List<Msg> messages;
    private String model = "deepseek-chat";
    private Integer max_tokens = 4096;
    private Boolean stream = true;
    private Object stream_options = null;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Msg {
        private String content;
        private String role;
    }
}

 4.2、前端

后端写好接口之后,前端直接用fetch请求访问即可,或者由于后端是直接用的controller接口,并且可以自定义响应体,也可以直接用自己自定义的axios请求,但若是遇到流式数据,需要自己自行处理。这里的请求体的构建以dify的接口需要为例。

接收流式数据,这里展示如何发送fetch请求给后端并处理返回的流式数据,完整的代码在下文给出。

const [controls, setControls] = useState<ChatControls>({
        abortController: null,
        currentAiMessageId: null
    });
//对话的id,用于区分不同的对话
const conversationId = useRef('')
//用户信息,替换为自己的,或者直接={username:"root"}也行
const userInfo = useUserInfo()

// 发送消息处理
    const sendMsg = async () => {
        if (!input.trim() || controls.currentAiMessageId) return;
        // 创建新的AbortController,用于中断fetch请求
        const abortController = new AbortController();
        setControls(c => ({...c, abortController}));

        //发送给AI的信息,测试的话直接const userMsg = "你好"
        const userMsg = input.trim()

        const obj = {
            "inputs": "",
            "query": userMsg,
            "response_mode": "streaming",
            "conversation_id": conversationId.current,
            "user": userInfo.username
        };

        try {
            //请求的发送,这里的url即为后端的地址http://xxxxxx:xxxx/agent/chat
            const response = await fetch(url, {
                method: 'POST',
                headers: headers,
                body: JSON.stringify(obj),
                signal: abortController.signal
            });
            const reader = response.body?.getReader();
            const decoder = new TextDecoder();
            let buffer = '';

            while (reader) {
                const {done, value} = await reader.read();
                if (done || end) {
                    // 处理最后剩余的数据
                    if (buffer.trim()) {
                        console.log(buffer)
                    }
                    break
                }

                const chunk=decoder.decode(value, {stream: true});
                //处理sse流式响应的封装信息
                buffer += chunk.replace(/^data: /, '');
                let lineEndIndex;
                while ((lineEndIndex = buffer.indexOf('\n')) >= 0) {
                    const line = buffer.slice(0, lineEndIndex).trim();
                    buffer = buffer.slice(lineEndIndex + 1);
                    if (line.startsWith('data:')) {
                        console.log(line.slice(5)); // 去掉"data:"
                    }else{
                        console.log(line);
                    }
                }
            }

        } catch (error: any) {
            if (error.name === 'AbortError') {
                console.log('请求已被中止');
            } else {
                console.error('请求失败:', error);
            }
        } finally {
            setControls(c => ({...c, abortController: null, currentAiMessageId: null}));
        }
    };

 如果是接收块级数据,直接接收即可

//该处的url为后端返回块级数据的接口http://xxxxxx:xxxx/agent/advice
const response=await fetchWithTimeout(url,{
                method: 'POST',
                headers: headers,
                body: JSON.stringify(obj),
            },30000)
const data = await response.json();
console.log(data)

其中fetchWithTimeout函数为自定义的带超时控制的fetch函数,防止块级数据响应时间过长,直接使用fetch也是一样的,函数如下

/**
 * 带有超时控制的fetch函数
 * @param resource
 * @param options
 * @param timeout
 * @param doTask
 */
export const fetchWithTimeout=async (resource: RequestInfo, options: RequestInit = {}, timeout = 8000,doTask?:()=>void): Promise<Response>=>{
    const controller = new AbortController();
    const id = setTimeout(() => {
        controller.abort()
        doTask&&doTask();
    }, timeout);

    try {
        const response = await fetch(resource, {
            ...options,
            signal: controller.signal
        });

        clearTimeout(id);
        return response;
    } catch (error) {
        throw error;
    }
}

至此,大模型API返回流式数据和块级数据的前后端联调方式已经展示完毕。

五、聊天界面搭建示例

聊天界面的前端代码以React为例,但由于请求的发送和接收是用的JS,因此Vue的用户改一下一下响应式变量,或者直接让AI将React代码转成Vue代码即可,逻辑是一样的。

页面示例

完整代码如下

chatPage/index.tsx

import {memo, useCallback, useEffect, useRef, useState} from 'react'
import type {ReactNode, FC} from 'react'
import {marked} from "marked";
import {useTranslation} from "react-i18next";
import {IconButton} from "@/components/icon";
import {MinusOutlined} from "@ant-design/icons";
import {Button, Input} from "antd";
import {themeVars} from "@/theme/theme.css.ts";
import {useUserInfo} from "@/store/userStore.ts";
import * from "./style.ts"

interface IProps {
    children?: ReactNode,
    toggleChat: () => void
}

// 类型定义
type Message = {
    id: string;
    role: 'user' | 'ai';
    content: string;
    thinkContent?: string;
    isComplete: boolean;
    thinkExpanded?: boolean;
    thinkTime?: number;
    isThinking?: boolean;
};

type ChatControls = {
    abortController: AbortController | null;
    currentAiMessageId: string | null;
};

const ChatPage: FC<IProps> = (props) => {
    const [messages, setMessages] = useState<Message[]>([]);
    const [input, setInput] = useState('');
    const [showScrollButton, setShowScrollButton] = useState(false);
    const [controls, setControls] = useState<ChatControls>({
        abortController: null,
        currentAiMessageId: null
    });
    const messagesEndRef = useRef<HTMLDivElement>(null);
    const messagesWrapperRef = useRef<HTMLDivElement>(null);
    const isAutoScrolling = useRef(true);
    //对话的id,用于区分不同的对话
    const conversationId = useRef('')
    //是否为带思考的信息
    const isThink = useRef(false);

    const {toggleChat} = props
    const {t} = useTranslation();
    const userInfo = useUserInfo()

    useEffect(() => {
        // 滚动事件处理
        const wrapper = messagesWrapperRef.current;
        const handleScroll = () => {
            if (!wrapper) return;
            const {scrollTop, scrollHeight, clientHeight} = wrapper;
            const isNearBottom = scrollHeight - (scrollTop + clientHeight) < 50;
            setShowScrollButton(!isNearBottom);
            isAutoScrolling.current = isNearBottom;
        };
        wrapper?.addEventListener('scroll', handleScroll);
        return () => wrapper?.removeEventListener('scroll', handleScroll);
    }, []);

    // 自动滚动处理
    const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
        messagesEndRef.current?.scrollIntoView({behavior});
        setShowScrollButton(false)
    }, []);

    // 消息更新时自动滚动
    useEffect(() => {
        if (isAutoScrolling.current) {
            scrollToBottom('auto');
        }
    }, [messages, scrollToBottom]);

    // 新建对话处理
    const handleNewChat = useCallback(() => {
        // 中止正在进行的请求
        if (controls.abortController) {
            controls.abortController.abort();
        }
        // 重置所有状态
        setMessages([]);
        setInput('');
        setControls({abortController: null, currentAiMessageId: null});
        setShowScrollButton(false)
        conversationId.current = ''
        isThink.current = false;
    }, [controls.abortController]);

    // 停止生成处理
    const handleStopGeneration = useCallback(() => {
        if (controls.abortController) {
            controls.abortController.abort();
        }
    }, [controls.abortController]);

    const url = import.meta.env.VITE_APP_CHAT_URL;
    const headers = new Headers({
        "Content-type": "application/json",
    });

    // 发送消息处理
    const sendMsg = async () => {
        if (!input.trim() || controls.currentAiMessageId) return;
        // 创建新的AbortController
        const abortController = new AbortController();
        setControls(c => ({...c, abortController}));

        const userMsg = input.trim()

        // 添加用户消息
        const userMessage: Message = {
            id: Date.now().toString(),
            role: 'user',
            content: userMsg,
            isComplete: true,
        };

        // 添加初始AI消息
        const aiMessage: Message = {
            id: `ai_${Date.now()}`,
            role: 'ai',
            content: '',
            isComplete: false,
            thinkExpanded: true,
            isThinking: true
        };

        setMessages(prev => [...prev, userMessage, aiMessage]);
        setInput('');

        const obj = {
            "inputs": "",
            "query": userMsg,
            "response_mode": "streaming",
            "conversation_id": conversationId.current,
            "user": userInfo.username
        };

        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: headers,
                body: JSON.stringify(obj),
                signal: abortController.signal
            });
            const reader = response.body?.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            let end = false
            //回复的信息
            let accumulatedData=''
            //思考的信息
            let thinkData=''
            //思考时间
            let thinkTime = 0
            //是否正在思考
            let isThinking=true
            //是否展开思考信息
            let thinkExpanded=true
            
            //对流式数据进行数据处理
            const handleDataLine = (jsonStr: string) => {
                try {
                    const data = JSON.parse(jsonStr);
                    if (data.event === 'message_end') {
                        end = true
                    }
                    if (data.conversation_id) {
                        conversationId.current = data.conversation_id
                    }
                    if (data.event === 'message' && data.answer) {
                        if (data.answer === "<think>") {
                            isThinking = true
                            isThink.current = true
                            thinkExpanded=true
                            thinkTime = new Date().getTime()
                        } else if (data.answer === "</think>") {
                            isThinking = false
                            thinkExpanded=false
                            thinkTime = Number(((new Date().getTime() - thinkTime) / 1000).toFixed(2))
                        }
                        if (isThinking) {
                            thinkData += data.answer
                        } else {
                            accumulatedData += data.answer;
                        }
                    }
                } catch (e) {
                    console.error('解析错误:', jsonStr, e);
                }
            }

            while (reader) {
                const {done, value} = await reader.read();
                if (done || end) {
                    // 处理最后剩余的数据
                    if (buffer.trim()) {
                        handleDataLine(buffer.trim());
                    }
                    break
                }

                const chunk=decoder.decode(value, {stream: true});
                buffer += chunk.replace(/^data: /, '');
                let lineEndIndex;
                while ((lineEndIndex = buffer.indexOf('\n')) >= 0) {
                    const line = buffer.slice(0, lineEndIndex).trim();
                    buffer = buffer.slice(lineEndIndex + 1);
                    if (line.startsWith('data:')) {
                        handleDataLine(line.slice(5)); // 去掉"data:"
                    }else{
                        handleDataLine(line);
                    }
                }

                // 更新AI消息内容
                setMessages(prev => prev.map(msg => {
                    if (msg.id === aiMessage.id) {
                        return {
                            ...msg,
                            content: accumulatedData,
                            thinkContent: thinkData,
                            thinkExpanded: thinkExpanded,
                            isComplete: false,
                            isThinking: isThinking,
                        };
                    }
                    return msg;
                }));
            }

            // 标记消息完成
            setMessages(prev => prev.map(msg => {
                if (msg.id === aiMessage.id) {
                    return {...msg,
                        isComplete: true,
                        thinkTime:thinkTime};
                }
                return msg;
            }));

        } catch (error: any) {
            if (error.name === 'AbortError') {
                console.log('请求已被中止');
                // 更新最后一条AI消息为完成状态
                setMessages(prev => prev.map(msg =>
                    msg.id === controls.currentAiMessageId
                        ? {...msg, isComplete: true}
                        : msg
                ));
            } else {
                console.error('请求失败:', error);
            }
        } finally {
            setControls(c => ({...c, abortController: null, currentAiMessageId: null}));
        }
    };

    // 处理思考信息展开和收缩事件
    const handleThinkClick = (messageId: string) => {
        setMessages(prev => prev.map(msg =>
            msg.id === messageId
                ? {...msg, thinkExpanded: !msg.thinkExpanded}
                : msg
        ));
    };

    return (
        <>
            <Container
                style={{
                    backgroundColor: themeVars.colors.background.paper,
                }}>
                <ButtonWrapper>
                    <Button type="primary" onClick={handleNewChat}>
                        {t('sys.chat.newChat')}
                    </Button>
                    <IconButton onClick={toggleChat}>
                        <MinusOutlined/>
                    </IconButton>
                </ButtonWrapper>


                <MessagesWrapper ref={messagesWrapperRef}>
                    {messages.length === 0 &&
                        <span className='tips' style={{color: themeVars.colors.text.primary}}>
                            {t('sys.chat.tips')}
                        </span>
                    }
                    {messages.map((message) => {
                        return (
                            <MessageItem key={message.id} $isUser={message.role === 'user'}>
                                <Avatar/>
                                <Bubble
                                    $isUser={message.role === 'user'}
                                    style={{
                                        backgroundColor: message.role === 'user' ? themeVars.colors.palette.primary.default : themeVars.colors.background.neutral,
                                        color: message.role === 'user' ? themeVars.colors.common.white : themeVars.colors.text.primary
                                    }}>
                                    {isThink.current && message.role !== 'user' &&
                                        <ThinkBg
                                            style={{
                                                backgroundColor: themeVars.colors.background.neutral,
                                                color: themeVars.colors.text.secondary
                                            }}
                                            $isExpanded={message.thinkExpanded || false}
                                            onClick={() => {
                                                handleThinkClick(message.id)
                                            }}>
                                            {message.thinkExpanded ? (
                                                // 展开状态显示原始内容
                                                <div
                                                    dangerouslySetInnerHTML={{__html: marked.parse(message.thinkContent || '')}}/>
                                            ) : (
                                                // 收缩状态显示自定义内容
                                                !message.isThinking ? (
                                                    <div className="custom-content">
                                                        <span>▶ {t('sys.chat.thingEnd')} {message.thinkTime} s</span>
                                                        <small>{t('sys.chat.openDetails')}</small>
                                                    </div>
                                                    ) : null
                                            )}
                                        </ThinkBg>
                                    }
                                    <div dangerouslySetInnerHTML={{__html: marked.parse(message.content)}}/>
                                </Bubble>
                            </MessageItem>
                        );
                    })}
                    <div ref={messagesEndRef}/>

                    {showScrollButton && (
                        <ScrollToBottomButton
                            style={{
                                backgroundColor: themeVars.colors.palette.primary.default,
                                color: themeVars.colors.common.white
                            }}
                            onClick={() => scrollToBottom()}
                        >
                            ↓
                        </ScrollToBottomButton>
                    )}
                </MessagesWrapper>

                <InputWrapper style={{backgroundColor: themeVars.colors.background.neutral}}>
                    <Input
                        style={{
                            flex: 1,
                            padding: "12px",
                            border: "1px solid #e0e0e0",
                            outline: "none",
                        }}
                        placeholder={t('sys.chat.input')}
                        value={input}
                        onChange={(e) => setInput(e.target.value)}
                        onKeyPress={(e) => e.key === 'Enter' && sendMsg()}
                    />
                    {controls.abortController ? (
                        <StopButton onClick={handleStopGeneration}>
                            {t('sys.chat.stop')}
                        </StopButton>
                    ) : (
                        <SendButton
                            style={{
                                backgroundColor: themeVars.colors.palette.primary.default,
                                color: themeVars.colors.common.white
                            }}
                            onClick={sendMsg}>
                            {t('sys.chat.send')}
                        </SendButton>
                    )}
                </InputWrapper>
            </Container>
        </>
    )
}

export default memo(ChatPage)

样式代码,chatPage/style.ts

import styled from 'styled-components';


export const Container = styled.div`
    display: flex;
    flex-direction: column;
    height: 100%;
    width: 100%;
    padding: 16px;
`;

export const ButtonWrapper = styled.div`
    display: flex;
    justify-content: space-between;
`;

export const MessagesWrapper = styled.div`
    flex: 1;
    overflow-y: auto;
    padding: 20px;
    position: relative;

    .tips {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        user-select: none;
    }
`;

export const MessageItem = styled.div<{ $isUser: boolean }>`
    display: flex;
    flex-direction: ${({$isUser}) => ($isUser ? 'row-reverse' : 'row')};
    margin-bottom: 20px;
    align-items: flex-start;
    gap: 12px;
`;

export const Avatar = styled.div`
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background-color: #e0e0e0;
    flex-shrink: 0;
`;

export const Bubble = styled.div<{ $isUser: boolean }>`
    max-width: 70%;
    padding: 12px 16px;
    border-radius: ${({$isUser}) =>
            $isUser ? '12px 12px 0 12px' : '12px 12px 12px 0'};
    position: relative;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`;

export const ThinkBg = styled.div<{ $isExpanded: boolean }>`
    padding: 5px;
    margin-bottom: 10px;
    max-height: ${({$isExpanded}) => ($isExpanded ? '300px' : '30px')};
    overflow: ${({$isExpanded}) => ($isExpanded ? 'auto' : 'hidden')};
    transition: all 0.3s ease;
    cursor: pointer;
    position: relative;

    &:hover {
        filter: brightness(0.95);
    }
`;

export const ScrollToBottomButton = styled.button`
    position: sticky;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    padding: 8px 16px;
    border: none;
    border-radius: 20px;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
`;

export const InputWrapper = styled.div`
    display: flex;
    gap: 12px;
    padding: 16px;
    background-color: white;
    border-radius: 8px;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
`;

export const SendButton = styled.button`
    padding: 12px 24px;
    border: none;
    border-radius: 8px;
    cursor: pointer;

    &:hover {
        opacity: 0.8;
    }
`;

export const StopButton = styled.button`
    padding: 12px 24px;
    background-color: #ff4d4f;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    transition: opacity 0.2s;

    &:hover {
        opacity: 0.8;
    }
`;

至此,所有流程已打通,若要更多自定义需求自行处理数据格式以及前端样式即可。

Logo

在这里,我们一起交流AI,学习AI,用AI改变世界。如有AI产品需求,可访问讯飞开放平台,www.xfyun.cn。

更多推荐