Qwen2.5-1.5B Streamlit界面美化:CSS注入+图标替换+响应式布局调整
Qwen2.5-1.5B Streamlit界面美化:CSS注入+图标替换+响应式布局调整
1. 为什么原生Streamlit聊天界面需要“再加工”
Streamlit开箱即用的st.chat_message和st.chat_input组件确实省去了前端框架搭建的麻烦,但默认界面离“专业级本地AI助手”仍有明显距离——气泡样式单调、图标缺乏语义、移动端显示错位、侧边栏与主区视觉失衡、消息时间戳缺失、输入框交互反馈弱……这些细节累积起来,会显著削弱用户对产品专业性和可靠性的信任感。
更关键的是,美观不是装饰,而是体验的一部分。一个清晰区分用户与AI消息的气泡系统,能让多轮对话逻辑一目了然;一个在手机横屏/竖屏下都保持可读的布局,决定了用户是否愿意在通勤路上继续提问;一个点击后有明确视觉反馈的「清空对话」按钮,能避免用户误操作后的焦虑。这些都不是锦上添花,而是本地化AI工具走向日常使用的必经之路。
本文不讲模型原理,不调超参,只聚焦一个务实目标:在不修改Streamlit核心逻辑、不引入额外JS框架的前提下,用纯CSS注入+轻量HTML定制+响应式断点控制,把Qwen2.5-1.5B的Streamlit界面,打磨成真正拿得出手的本地对话产品。
2. CSS注入:从全局重置到局部精修
2.1 安全注入CSS的三种方式对比
Streamlit支持多种CSS注入方式,但并非都适合生产环境:
| 方式 | 代码示例 | 适用性 | 风险提示 |
|---|---|---|---|
st.markdown(..., unsafe_allow_html=True) | <style>body{...}</style> | 快速验证 | 样式作用域宽泛,易污染全局 |
st.html()(v1.32+) | st.html("<style>...</style>") | 推荐首选 | 作用域可控,无XSS风险 |
外部CSS文件 + st.components.v1.html | 加载本地CSS | 本项目不采用 | 增加部署复杂度,需额外路径管理 |
我们选择st.html()注入,既安全又灵活。将所有自定义样式封装为字符串,在main()函数最顶部一次性注入:
import streamlit as st
def inject_custom_css():
css = """
<style>
/* 全局字体与重置 */
:root {
--primary-color: #1677ff;
--user-bubble-bg: #e6f7ff;
--ai-bubble-bg: #f0f0f0;
--border-radius: 12px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
}
/* 移除默认padding/margin干扰 */
.block-container { padding-top: 1rem; padding-bottom: 2rem; }
.stApp > div:first-child { padding: 0; }
/* 聊天消息气泡基础样式 */
.stChatMessage {
margin-bottom: 1.2rem;
max-width: 90%;
}
/* 用户消息气泡 */
.stChatMessage[data-test-id="user"] {
margin-left: auto;
background-color: var(--user-bubble-bg);
border-radius: var(--border-radius) var(--border-radius) 0 var(--border-radius);
box-shadow: var(--shadow-sm);
}
/* AI消息气泡 */
.stChatMessage[data-test-id="assistant"] {
margin-right: auto;
background-color: var(--ai-bubble-bg);
border-radius: var(--border-radius) var(--border-radius) var(--border-radius) 0;
box-shadow: var(--shadow-sm);
}
/* 消息内容文字样式 */
.stChatMessage .stMarkdown p {
margin: 0.5rem 0;
line-height: 1.5;
font-size: 0.95rem;
}
/* 输入框增强 */
.stTextInput input {
border-radius: 8px;
padding: 0.6rem 1rem;
border: 1px solid #d9d9d9;
transition: all 0.2s;
}
.stTextInput input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2);
}
/* 侧边栏按钮统一风格 */
.stSidebar [data-testid="stButton"] button {
width: 100%;
border-radius: 8px;
padding: 0.6rem 0;
font-weight: 500;
margin-top: 0.5rem;
border: none;
background-color: #f5f5f5;
color: #333;
text-align: left;
padding-left: 1rem;
}
.stSidebar [data-testid="stButton"] button:hover {
background-color: #e6f7ff;
color: var(--primary-color);
}
/* 时间戳样式(后续通过JS注入) */
.message-timestamp {
font-size: 0.75rem;
color: #999;
margin-top: 0.3rem;
text-align: right;
font-style: italic;
}
</style>
"""
st.html(css)
# 在应用入口处调用
if __name__ == "__main__":
inject_custom_css()
# 后续主逻辑...
2.2 关键样式设计逻辑说明
- 气泡方向与圆角:通过
margin-left: auto和margin-right: auto强制左右对齐,配合不对称border-radius实现微信式气泡造型,比纯flex布局更稳定; - 阴影与间距:仅使用
box-shadow: var(--shadow-sm)而非border,避免在高DPI屏幕下出现模糊边框; - 字体继承:未重写
font-family,确保与Streamlit默认字体(Inter)一致,避免中文字体渲染异常; - 移动端适配预留:所有尺寸单位使用
rem和%,为后续媒体查询打下基础。
3. 图标替换:用SVG替代emoji,提升专业感与可访问性
原项目中使用的🧹 清空对话、🧠 Qwen2.5-1.5B等emoji存在三大问题:在部分Linux终端渲染异常、色盲用户识别困难、无法通过CSS控制大小与颜色。我们改用内联SVG图标,完全可控:
3.1 侧边栏图标系统重构
def render_sidebar():
with st.sidebar:
st.title(" Qwen2.5-1.5B 助手")
# 自定义SVG图标:清空对话
st.markdown("""
<div style="display: flex; align-items: center; gap: 0.5rem; margin: 0.5rem 0;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H5H21" stroke="#1677ff" stroke-width="2" stroke-linecap="round"/>
<path d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6M21 12H3" stroke="#1677ff" stroke-width="2" stroke-linecap="round"/>
</svg>
<button onclick="window.location.reload()" style="background: none; border: none; color: #1677ff; font-weight: 500; cursor: pointer; padding: 0;">
清空对话
</button>
</div>
""", unsafe_allow_html=True)
# 模型信息卡片(含SVG图标)
st.markdown("""
<div style="background: #f0f9ff; border-radius: 8px; padding: 1rem; margin-top: 1rem;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C13.183 2 14.15 2.967 14.15 4.15C14.15 5.333 13.183 6.3 12 6.3C10.817 6.3 9.85 5.333 9.85 4.15C9.85 2.967 10.817 2 12 2ZM21 9V7C21 5.817 20.033 4.85 18.85 4.85C17.667 4.85 16.7 5.817 16.7 7V9C16.7 10.183 17.667 11.15 18.85 11.15C20.033 11.15 21 10.183 21 9ZM3 9V7C3 5.817 3.967 4.85 5.15 4.85C6.333 4.85 7.3 5.817 7.3 7V9C7.3 10.183 6.333 11.15 5.15 11.15C3.967 11.15 3 10.183 3 9ZM12 12.3C14.926 12.3 17.3 14.674 17.3 17.6C17.3 20.526 14.926 22.9 12 22.9C9.074 22.9 6.7 20.526 6.7 17.6C6.7 14.674 9.074 12.3 12 12.3Z" fill="#1677ff"/>
</svg>
<strong>Qwen2.5-1.5B-Instruct</strong>
</div>
<p style="font-size: 0.85rem; color: #595959; margin: 0;">1.5B参数 · 本地推理 · 零数据上传</p>
</div>
""", unsafe_allow_html=True)
3.2 聊天消息图标增强
在每条消息气泡左上角添加角色标识图标(用户/机器人),强化身份认知:
def display_message(role: str, content: str):
# 时间戳生成(精确到秒)
from datetime import datetime
timestamp = datetime.now().strftime("%H:%M")
if role == "user":
st.markdown(f"""
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.3rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12C14.2091 12 16 10.2091 16 8C16 5.79086 14.2091 4 12 4C9.79086 4 8 5.79086 8 8C8 10.2091 9.79086 12 12 12Z" stroke="#1677ff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02936 7.02944 3 12 3C16.9706 3 21 7.02936 21 12Z" stroke="#1677ff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span style="font-size: 0.85rem; color: #595959;">你</span>
</div>
<div style="margin-left: 2rem; margin-top: -0.5rem;">
{content}
</div>
<div class="message-timestamp">{timestamp}</div>
""", unsafe_allow_html=True)
else:
st.markdown(f"""
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.3rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C13.183 2 14.15 2.967 14.15 4.15C14.15 5.333 13.183 6.3 12 6.3C10.817 6.3 9.85 5.333 9.85 4.15C9.85 2.967 10.817 2 12 2ZM21 9V7C21 5.817 20.033 4.85 18.85 4.85C17.667 4.85 16.7 5.817 16.7 7V9C16.7 10.183 17.667 11.15 18.85 11.15C20.033 11.15 21 10.183 21 9ZM3 9V7C3 5.817 3.967 4.85 5.15 4.85C6.333 4.85 7.3 5.817 7.3 7V9C7.3 10.183 6.333 11.15 5.15 11.15C3.967 11.15 3 10.183 3 9ZM12 12.3C14.926 12.3 17.3 14.674 17.3 17.6C17.3 20.526 14.926 22.9 12 22.9C9.074 22.9 6.7 20.526 6.7 17.6C6.7 14.674 9.074 12.3 12 12.3Z" fill="#1677ff"/>
</svg>
<span style="font-size: 0.85rem; color: #595959;">Qwen</span>
</div>
<div style="margin-left: 2rem; margin-top: -0.5rem;">
{content}
</div>
<div class="message-timestamp">{timestamp}</div>
""", unsafe_allow_html=True)
4. 响应式布局调整:让界面在手机/平板/桌面同样优雅
Streamlit默认布局在小屏下会出现横向滚动、气泡溢出、按钮被截断等问题。我们通过媒体查询精准控制:
4.1 主要断点策略
/* 响应式断点 */
@media (max-width: 768px) {
/* 移动端:窄气泡 + 更大间距 */
.stChatMessage {
max-width: 95%;
margin-bottom: 1.5rem;
}
.stChatMessage[data-test-id="user"] {
margin-left: auto;
border-radius: var(--border-radius) var(--border-radius) 0 12px;
}
.stChatMessage[data-test-id="assistant"] {
margin-right: auto;
border-radius: var(--border-radius) var(--border-radius) 12px 0;
}
/* 输入框占满宽度 */
.stTextInput input {
width: 100%;
padding: 0.7rem 1rem;
}
/* 侧边栏折叠为顶部导航 */
.stSidebar {
display: none;
}
/* 顶部添加简易导航栏 */
.mobile-header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: white;
padding: 0.75rem 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
}
.mobile-header h2 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
/* 平板:适度缩放 */
.stChatMessage {
max-width: 85%;
}
.stTextInput input {
padding: 0.65rem 1rem;
}
.stSidebar [data-testid="stButton"] button {
padding: 0.55rem 0;
font-size: 0.9rem;
}
}
@media (min-width: 1025px) {
/* 桌面:保持默认,仅微调 */
.stChatMessage {
max-width: 80%;
}
}
4.2 移动端专属导航栏注入
在CSS注入后,动态添加移动端头部:
def render_mobile_header():
st.html("""
<div class="mobile-header">
<h2> Qwen助手</h2>
<button onclick="document.querySelector('.stSidebar').style.display='block'"
style="background: none; border: none; color: #1677ff; font-weight: 500;">
⚙ 设置
</button>
</div>
<style>
@media (min-width: 769px) {
.mobile-header { display: none; }
}
</style>
""", unsafe_allow_html=True)
# 在inject_custom_css()之后调用
render_mobile_header()
5. 实用增强技巧:让美化不止于“好看”
5.1 消息流平滑滚动(防抖优化)
原生Streamlit在快速追加消息时会出现滚动卡顿。我们用JavaScript实现防抖滚动:
def scroll_to_bottom():
st.html("""
<script>
// 防抖滚动函数
let scrollTimer;
function smoothScrollToBottom() {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
const chatContainer = document.querySelector('.stMainBlock');
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}, 50);
}
// 监听新消息插入
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
smoothScrollToBottom();
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
</script>
""", unsafe_allow_html=True)
5.2 输入框回车提交 + 禁用空消息
# 替代原生st.chat_input
prompt = st.chat_input("向Qwen提出你的问题...", key="chat_input")
if prompt and prompt.strip():
# 添加到历史并触发推理
st.session_state.messages.append({"role": "user", "content": prompt})
# ...后续处理逻辑
else:
st.warning("请输入有效内容")
5.3 错误状态友好提示
当模型加载失败或推理超时时,用带图标的警示框替代红色报错:
st.error("""
<div style="display: flex; align-items: center; gap: 0.5rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C13.183 2 14.15 2.967 14.15 4.15C14.15 5.333 13.183 6.3 12 6.3C10.817 6.3 9.85 5.333 9.85 4.15C9.85 2.967 10.817 2 12 2ZM21 9V7C21 5.817 20.033 4.85 18.85 4.85C17.667 4.85 16.7 5.817 16.7 7V9C16.7 10.183 17.667 11.15 18.85 11.15C20.033 11.15 21 10.183 21 9ZM3 9V7C3 5.817 3.967 4.85 5.15 4.85C6.333 4.85 7.3 5.817 7.3 7V9C7.3 10.183 6.333 11.15 5.15 11.15C3.967 11.15 3 10.183 3 9ZM12 12.3C14.926 12.3 17.3 14.674 17.3 17.6C17.3 20.526 14.926 22.9 12 22.9C9.074 22.9 6.7 20.526 6.7 17.6C6.7 14.674 9.074 12.3 12 12.3Z" fill="#ff4d4f"/>
</svg>
<strong>模型加载异常</strong>:请检查模型路径 `/root/qwen1.5b` 是否存在完整文件,或尝试重启服务。
</div>
""", unsafe_allow_html=True)
6. 总结:一套可复用的Streamlit界面升级范式
本次Qwen2.5-1.5B界面美化实践,验证了一套轻量、安全、可持续的Streamlit UI升级路径:
- CSS注入不破不立:用
st.html()替代unsafe_allow_html,规避XSS风险,同时获得完整的CSS作用域控制能力; - 图标系统去emoji化:所有功能图标均采用内联SVG,尺寸、颜色、语义完全可控,且天然支持无障碍阅读;
- 响应式非“一刀切”:针对手机/平板/桌面三类设备分别优化,移动端隐藏侧边栏、增加顶部导航,平板适度缩放,桌面保持专业密度;
- 增强功能不依赖第三方库:平滑滚动、防空提交、错误提示等均用原生JS+HTML实现,零npm依赖,部署即用。
这套方案已沉淀为可复用的ui_utils.py模块,未来接入Qwen2.5-7B或其它本地模型时,只需替换主题色变量与图标路径,30分钟即可完成界面焕新。真正的本地AI体验,不该止步于“能跑”,而应追求“愿用”——当用户第一次打开界面就感受到专业与温度,技术的价值才真正落地。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐


所有评论(0)