重生之我是前端大佬

vibe coding针不戳

前段时间用streamlit来写前后端一体的大模型网页,见之前的blog,但是实际使用上加载很慢。为了加快访问速度,同时支持curl命令行直接调用,我决定换回FASTAPI,同时自行编写html文件。因为FASTAPI支持返回文件作为响应,在后端部分就很容易实现了。只需要在原有的FASTAPI基础上加入下面的代码:

@app.get("/", response_class=FileResponse)
async def read_root():
    # 假设 index.html 文件位于当前目录
    file_path = os.path.join(os.path.dirname(__file__), "index.html")
    return FileResponse(file_path)

同时需要在当前目录下面加入index.html文件。接下来就到大模型大展身手的时候了,直接告诉deepseek你的需求,然后让它自己生成html文件即可。当然有些部分还需要修改,这时候可以一步步来让deepseek进行修改。为了实现某些界面功能,css样式和javascript需要同时修改,这里大模型可能会出现搞不定的情况,就需要开发者对css和javascript有一定的了解。例如,我想在搭建的大模型网页上实现一个在生成时出现转圈圈的效果。需要在特定的位置放置该元素,另外还要调整大小以适合当前页面布局。从css选择器到样式动态效果,再到页面添加时到逻辑,这些一连串的组成部分目前并不能通过大模型的one-shot来完成,还是需要人为地拆解和处理, 下面就来介绍一下具体实现。

对话框内的效果

因为页面上大模型进行回答时,对话框体内是先添加了包含对话头像的div元素,然后在div内加入回答。需求是在开始回答时就在头像后面加入转圈圈,流式回答不断更新对话框回答内容,回答完毕时移除转圈圈的元素。所以首先要获取到对话框体的div,这里我直接在创建对话框体时返回了对应的div,如下:

    const contentDiv = document.createElement('div');
    contentDiv.classList.add('message-content');
    contentDiv.innerHTML = renderMarkdownAndLatex(message);

    ...

    // 将消息添加到聊天框中
    chatBox.appendChild(messageDiv);
    // 触发KaTeX渲染和代码高亮
    renderMathInElement(contentDiv);
    hljs.highlightAll();
    chatBox.scrollTop = chatBox.scrollHeight; // 滚动到底部
    return contentDiv;

这样的话,就可以通过修改contentDiv.innerHTML来对流式回答的内容进行实时更新。另外因为deepseek有思考的过程,思考时的内容文字需要不一样的样式,为了方便,我直接将思考的内容每行前加入一个> 来变成类似于markdown里面引用的效果。但是经过marked.js转换之后,就会变成blockquote元素,那就直接用css来修改一下样式就行,如下:

blockquote{
    color:#a6a6a6;
    border-left:2px solid #e5e5e5;
    height:calc(100% - 10px);
    margin-top: 5px;
    margin-left: 0px;
    padding-left: 10px;
}

接下来说说转圈圈。定义一个loader的效果,又是需要css来

/* 加载动画 */
.loader {
    width: 20px;
    height: 20px;
    margin: 10px 10px 10px 0px;
    border: 3px solid rgba(255,255,255,0.2);
    border-top-color: #00ff88;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

/* 旋转动画 */
@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

然后就是添加的位置和移除的时间:在一开始生成回答时添加,在生成完成时移除。这就涉及到javascript中创建和移除html元素的函数了,如下:

// 添加加载动画
if (isloading) {
    const loaderDiv = document.createElement('div');
    loaderDiv.classList.add('loader');
    messageDiv.appendChild(loaderDiv);
}

...

// 获取所有loader元素
const elements = document.getElementsByClassName('loader');
// 倒序循环避免实时集合问题
while(elements.length > 0) {
    elements[0].parentNode.removeChild(elements[0]);
}

通过document.createElement来创建元素和修改class,需要删除时可以通过document.getElementsByClassNamedocument.getElementsById来获取元素,然后从parentNode删除对应元素。

访问控制

UI交互页面上,开发者其实很多时候没有给用户很多自由性,因为会损坏到系统的安全性。从MVC模型的角度,就是对应了一个自动状态机。当然自动状态机是一个很强大的模型,但是实际上,为了某一个功能而实现的页面,状态数量可以很少,所以用户就在这些状态集合里打转转。在前端部分,可以对用户的操作加以限制,但是真正最有效的控制离不开后端的实现。例如,对某些API接口限制访问上限:

import asyncio

# 使用信号量进行限流
semaphore = asyncio.Semaphore(5)

...

@app.post("/generate")
async def generate_text(request: Request, api_key: str = Depends(api_key_header)):
    async with semaphore:
        if api_key not in auth_keys:
            raise HTTPException(status_code=403, detail="Invalid API Key")
        
        # 编码输入
        inputs = tokenizer(request.prompt, return_tensors="pt").to(model.device)
        

在前端部分,也需要对相应的按钮加以限制。例如,在发送了一个prompt后,发送按钮就变灰色了,暂时阻塞了继续请求。同时,在回答生成完毕后,按钮也要恢复,如下:

// 获取按钮元素
async function sendMessage() {
    // 获取按钮元素
    const btn = document.getElementById('btn-send');
    if(btn.disabled){
        alert('请等待上一个对话结果完成!');
        return;
    }
    // 禁用按钮
    btn.disabled = true;
    btn.style.backgroundColor = "#ccc";

    ...

    // 回答完成后恢复按钮
    btn.disabled = false;
    btn.style.backgroundColor = "#007bff";

最后分享一下我的简易版deepseek仓库页面

如果您觉得该文章对您有用,欢迎打赏作者,激励创作!
Welcome to tip the author!

微信(WeChat Pay) 支付宝(AliPay)
比特币(Bitcoin) 以太坊(Ethereum)
以太坊(Base) 索拉纳(Solana)