在上一篇 《告别黑框框:我用Streamlit,3小时给AI穿上了“钢铁侠战衣”》 中,我们体验了Streamlit的黑魔法,成功地将我们强大的AI内核,从冰冷的命令行,封装成了一个有血有肉的Web应用。它能看,能用,看起来已经很酷了。
但当我把这个应用的早期版本发给朋友试用时,我收到了三个尖锐的反馈:
‘我只是想拖动一下滑块,为什么整个页面都要重新加载一遍,烦死了!’
‘你的报告太长了,我只想看结论,能不能把那些技术图表先收起来?’
‘很酷,但我的外国朋友看不懂中文,能加个英文版吗?’
这些问题,直击要害。我意识到,一个好的应用,不仅要功能强大,更要体验流畅、重点突出、易于扩展。我的“钢铁侠战衣”还只是个粗糙的Mark I原型,是时候对它的**交互系统(UI/UX)**进行一次彻底的升级了。
一、交互的“痛点”:为什么我的应用感觉很“卡”?
要理解第一个问题,我们必须重温Streamlit的核心工作原理:当你与页面上的任何一个组件(滑块、单选框、输入框)进行交互时,Streamlit会从头到尾重新运行你的整个Python脚本。
这既是它的优点(逻辑简单),也是它的潜在缺点。想象一下我们的应用流程:
用户拖动了一下shot_count的滑块。
app.py脚本立刻重新运行。
load_all_models()被执行(幸好有@st.cache_resource缓存了)。
所有UI组件被重新渲染.,然后脚本结束,因为用户还没点击“分析”按钮。
这个过程就像你只是想调整一下汽车的后视镜,结果整辆车都熄火重启了一遍!
虽然速度很快,但对于复杂应用来说,这种不必要的‘全局刷新’会让用户感到烦躁和失控。
我需要一个‘结界’,在这个结界里,用户可以随意调整所有参数,而不会惊动后台的AI引擎。只有当用户按下‘确认’时,结界才会消失,并把所有最终参数一次性发送给后台。这个结界,就是st.form。
二、交互利器一:st.form - 创造一个“免打扰”的输入空间
深入理解st.form:
它是什么? st.form是Streamlit提供的一个上下文管理器。所有被包裹在with st.form(…)代码块内的Streamlit组件,都会被视为一个整体。
它的工作原理: 当你在form内部与组件(如滑块、输入框)交互时,这些组件的值会在前端即时更新,但不会触发Python脚本的重新运行。脚本会被“暂停”,静静地等待。
触发器: 只有当用户点击了位于这个form内部的st.form_submit_button时,这个“暂停”状态才会被解除。此时,Streamlit会做两件事:
将form内部所有组件的最终值,一次性地提交给后端。
重新运行整个Python脚本。在这次运行中,st.form_submit_button会返回True,我们就可以在if语句中捕获这个信号,执行真正的分析逻辑。
构建我们的输入表单:
在 app.py 中
# 使用st.form将所有输入组件包裹起来
with st.form("metadata_form"):
st.header("第二步:补充视频元数据")
title_input = st.text_input("视频标题")
col1, col2 = st.columns(2)
with col1:
duration_input = st.number_input("时长(秒)")
shot_count_input = st.number_input("镜头数")
with col2:
# 用户可以随意拖动这个滑块,页面不会刷新
skip_rate_input = st.slider("预估2秒跳过率 (%)", 0, 100, 40)
has_subtitle_input = st.radio("是否有字幕?", [1, 0])
# 这是唯一的触发器
submitted = st.form_submit_button("获取全面诊断报告")
# 只有当按钮被点击后,submitted才会变成True,下面的代码块才会执行
if submitted:
# 在这里执行所有耗时的分析和预测...
st.balloons() # 用一个庆祝动画来表示成功触发
# ...
实现了这个改动后,应用的体验发生了质的飞跃。用户可以在表单里随心所欲地调整各种参数,进行思考,而页面始终保持安静、稳定。只有当他们对所有参数都满意,并按下那个唯一的“分析”按钮时,后台的“AI巨兽”才会被唤醒。这种掌控感,是优秀用户体验的基础。
三、交互利器二:st.expander - 让信息有“呼吸感”
第二个问题是“信息过载”。我们的诊断报告非常全面,但如果把所有内容都平铺直叙地展示出来,用户会被淹没在信息的海洋里,抓不住重点。
我需要一个能对信息进行**“降噪”和“分层”**的工具。我需要一个“抽屉”,可以把次要的、或者技术性太强的信息先收起来,只把最重要的结论展示给用户。这个“抽屉”,就是st.expander。
深入理解st.expander:
它也是一个上下文管理器,所有被包裹在with st.expander(“标题”):代码块内的内容,都会被默认折叠起来。
用户只会看到一个可点击的“标题”。只有当他们对这部分内容感兴趣时,才会主动点击,展开查看详情。
它有一个非常有用的参数expanded=False(默认值)。我们可以通过逻辑判断,来动态决定某个“抽屉”在页面加载时是默认展开还是收起。
构建可折叠的诊断报告:
# 在 app.py 的 display_results 函数中
st.subheader("👨⚕️ AI创作医生全面诊断")
# 假设diagnoses是一个包含 (score, text) 元组的列表
for score, diagnosis_text in diagnoses:
# 从诊断文本的第一行提取出标题
expander_title = diagnosis_text.splitlines()[0]
# 提取除了标题以外的内容
content = "\n\n".join(diagnosis_text.splitlines()[1:]).strip()
# !! 核心逻辑:如果分数低于70分(即非良好或卓越),则默认展开这个抽屉 !!
with st.expander(expander_title, expanded=score < 70):
st.markdown(content)
# 将技术性最强的SHAP图也放入一个默认折叠的抽屉中
with st.expander("💡 归因诊断图 (技术细节)"):
st.pyplot(shap_plot)
经过这次改造,我们的诊断报告变得极其清爽和有重点。
用户第一眼看到的,只会是那些被AI判定为“有待改进”或“关键短板”的、被自动展开的诊断项。
他们可以快速聚焦于问题所在。而那些表现良好的项目,以及技术性很强的SHAP图,则被安静地收纳起来,等待用户在需要时自行探索。
这种“主次分明”的信息呈现方式,是对用户注意力的最大尊重。
四、交互利器三:st.session_state - 赋予应用“记忆力”
第三个问题是“多语言支持”。要实现这个功能,我们的应用必须能“记住”用户当前选择的是中文还是英文。如果每次交互都导致脚本重跑,而应用本身没有“记忆”,那么用户的语言选择就会丢失。
能够跨越多次“重新运行”来存储信息的变量,就是会话状态 (Session State)。在Streamlit中,它通过st.session_state这个类似字典的对象来实
深入理解st.session_state:
它是一个“魔法字典”: 你可以像使用普通Python字典一样,用st.session_state.my_variable = "hello"来赋值,用print(st.session_state.my_variable)来读取。
它的魔法在于“持久化”: 存入st.session_state中的任何数据,在当前用户的整个浏览器会话期间,都会被保留下来,无论脚本重新运行多少次。
典型应用: 存储用户登录状态、语言选择、多页面应用中的数据传递等。
构建多语言切换器:
# 在 app.py 的开头部分
# 1. 初始化会话状态
# 检查 'lang' 是否已经存在于session_state中,如果不存在,就设置一个默认值
if 'lang' not in st.session_state:
st.session_state.lang = 'zh' # 默认语言为中文
# --- 在侧边栏创建语言切换器 ---
st.sidebar.header("设置 / Settings")
selected_lang = st.sidebar.radio(
"Language / 语言",
['zh', 'en'],
# format_func让选项显示为更友好的文本
format_func=lambda x: "中文" if x == 'zh' else "English",
# index根据当前session_state中的语言来决定默认选中哪个
index=0 if st.session_state.lang == 'zh' else 1
)
# 2. 检查用户的选择是否发生了变化
if selected_lang != st.session_state.lang:
# 如果变了,就更新session_state中的值
st.session_state.lang = selected_lang
# !! 并立即调用st.rerun(),强制页面用新的语言设置重新加载一遍 !!
st.rerun()
# --- 在UI的任何地方,都使用tr()函数来获取翻译 ---
def tr(key):
# 这个函数会根据当前session_state中的语言,返回正确的文本
return TRANSLATIONS.get(key, {}).get(st.session_state.lang, key)
# 例如:
st.title(tr('main_title'))
通过st.session_state,我们的应用终于拥有了“记忆”。
用户在侧边栏切换一次语言后,这个选择就会被牢牢记住。
即使他们与页面上的其他组件交互,导致脚本重跑,应用也总能记得应该用哪种语言来渲染文本,从而实现了流畅、无缝的多语言体验
五、留下新的篇章
通过st.form, st.expander, 和 st.session_state这三件神器的加持,我们的AI顾问不仅功能强大,而且在交互体验上也达到了专业水准。它现在是一个懂礼貌、有重点、会记忆的“贴心管家”。
但是,我们至今为止所有的工作,都还停留在本地运行。这件华丽的“钢铁侠战衣”,还只能在我自己的电脑上发光发热。
互动: “我们已经打造了一辆性能和内饰都堪称顶级的超级跑车。现在,是时候把它开出我们的‘车库’,让全世界都看到它的风采了!你们觉得,一个好的个人项目,是应该‘藏在深闺人未识’,还是应该大胆地‘公之于众’,接受所有人的检验和赞美?
下一篇,我们将进入一个极其激动人心的工程实践篇——【V16.0 - 避坑篇】
这个篇文章源码需要,可以考虑在这里下载
我将手把手地教大家,如何将我们的Streamlit应用,一键部署到云端,生成一个任何人都可以通过网址访问的公开应用!同时,我也会毫无保留地分享我在整个开发过程中踩过的所有“坑”和总结出的“最佳实践”!敬请期待!