歡迎來到 open-notebook
教學系列的第三章!在前一章 模型管理器 (ModelManager) 中,我們學會了如何集中管理應用程式所需的各種 AI 模型,就像一個隨時待命的工具箱管理員。我們現在有了管理資料的 物件模型 (ObjectModel) 和管理 AI 工具的 ModelManager
。
但是,使用者如何與這些強大的工具和儲存的資料互動呢?他們如何看到自己的筆記本列表?如何新增筆記?如何觸發 AI 功能來產生摘要或與知識庫聊天?這就需要一個友善且直觀的「門面」——也就是我們的使用者介面。
想像一下,就算您擁有一輛引擎強大、功能齊全的汽車,但如果沒有方向盤、儀表板、油門和煞車踏板,您也無法駕駛它去任何地方。您需要一個控制面板來告訴汽車您想做什麼,並看到汽車目前的狀態(例如速度、油量)。
在 open-notebook
中,使用者介面 (UI) 就扮演著這個「駕駛艙」的角色。它提供了視覺元素和互動方式,讓使用者能夠:
如果沒有 UI,使用者就無法方便地使用 open-notebook
的各種功能。UI 是連接使用者和應用程式底層邏輯(如物件模型、模型管理器)的橋樑。
open-notebook
使用一個名為 Streamlit 的 Python 函式庫來建立其網頁使用者介面。Streamlit 的最大優點是它讓開發者可以用純 Python 程式碼快速建立互動式的網頁應用程式,而不需要編寫複雜的前端程式碼(如 HTML, CSS, JavaScript)。
您可以將 Streamlit 想像成一個數位樂高積木:
st.button
)、文字輸入框 (st.text_input
)、下拉選單 (st.selectbox
)、頁籤 (st.tabs
) 等。open-notebook
的 UI 程式碼: 使用這些積木來搭建應用程式的介面。open-notebook
的 Streamlit UI 主要包含以下幾個部分:
app_home.py
: 應用的主入口(通常重新導向)。pages/2_📒_Notebooks.py
: 顯示筆記本列表和單一筆記本內容的頁面。pages/3_🔍_Ask_and_Search.py
: 執行搜尋和問答功能的頁面。pages/7_🤖_Models.py
: 管理和設定 AI 模型的頁面。pages/
目錄下的 Python 檔案轉換成側邊欄的導覽項目。pages/components/note_panel.py
: 用於顯示和編輯單一筆記的面板。pages/components/source_panel.py
: 用於顯示和處理單一來源的面板。pages/stream_app/note.py
中的 note_card()
: 用於在列表中顯示筆記摘要卡片。pages/stream_app/source.py
中的 source_card()
: 用於在列表中顯示來源摘要卡片。透過組合這些頁面和元件,open-notebook
為使用者提供了一個互動式的介面來存取其所有功能。
讓我們以「查看筆記本列表並建立一個新筆記本」這個常見的使用情境為例,看看 Streamlit UI 是如何實現的。這個功能主要由 pages/2_📒_Notebooks.py
這個檔案處理。
當使用者點擊側邊欄的「📒 Notebooks」連結時,Streamlit 會執行 pages/2_📒_Notebooks.py
裡的程式碼。
# pages/2_📒_Notebooks.py (簡化片段)
import streamlit as st
from open_notebook.domain.notebook import Notebook # 匯入 Notebook 物件模型
from pages.stream_app.utils import setup_page # 匯入頁面設定輔助函式
# 設定頁面標題和基本配置
setup_page("📒 Open Notebook")
# ... 其他程式碼 ...
# 顯示主標題
st.title("📒 我的筆記本")
st.caption("筆記本是組織您的想法、點子和來源的好方法...")
程式碼解釋:
import streamlit as st
: 這是使用 Streamlit 的標準方式,將其匯入並簡稱為 st
。setup_page(...)
: 這是一個 open-notebook
自訂的輔助函式,用於設定一些通用的頁面屬性(像是瀏覽器標籤的標題、頁面寬度等),並執行一些檢查(例如檢查必要的模型是否已設定)。st.title(...)
: 使用 Streamlit 的 title
指令在頁面上顯示一個大標題。st.caption(...)
: 顯示一行小字體的說明文字。Streamlit 會將這些指令轉換成對應的 HTML 元素顯示在瀏覽器中。
為了顯示現有的筆記本,UI 程式碼需要從後端(也就是我們的 物件模型 (ObjectModel))取得資料。
# pages/2_📒_Notebooks.py (簡化片段)
from pages.stream_app.note import note_card # 匯入顯示筆記卡片的元件
from pages.stream_app.source import source_card # 匯入顯示來源卡片的元件
from pages.stream_app.utils import setup_stream_state # 匯入狀態管理輔助函式
from humanize import naturaltime # 用於顯示相對時間 (例如 "5分鐘前")
# ... (前面顯示標題的程式碼) ...
def notebook_list_item(notebook):
"""一個顯示單一筆記本摘要的函式 (元件)"""
with st.container(border=True): # 建立一個帶邊框的容器
st.subheader(notebook.name) # 顯示筆記本名稱 (子標題)
st.caption(
f"建立時間: {naturaltime(notebook.created)}, 更新時間: {naturaltime(notebook.updated)}"
)
st.write(notebook.description) # 顯示筆記本描述
# 顯示一個 "Open" 按鈕,key 是為了讓每個按鈕獨一無二
if st.button("開啟", key=f"open_notebook_{notebook.id}"):
# 如果按鈕被點擊,設定 session_state 並重新執行頁面
st.session_state["current_notebook_id"] = notebook.id
st.rerun()
# --- 主邏輯 ---
# 從資料庫獲取所有未封存的筆記本 (使用 ObjectModel 的功能)
notebooks = Notebook.get_all(order_by="updated desc")
active_notebooks = [nb for nb in notebooks if not nb.archived]
# 遍歷每個筆記本,並使用 notebook_list_item 函式顯示它
for notebook in active_notebooks:
notebook_list_item(notebook)
程式碼解釋:
notebook_list_item(notebook)
: 我們定義了一個函式,它接收一個 Notebook
物件,並使用 st.container
, st.subheader
, st.caption
, st.write
和 st.button
來顯示這個筆記本的資訊和一個「開啟」按鈕。這就是一個簡單的可重複使用元件。Notebook.get_all(...)
: 這裡呼叫了 物件模型 (ObjectModel) 提供的 get_all
類別方法,從資料庫中讀取所有的 Notebook
物件,並按更新時間排序。[nb for nb in notebooks if not nb.archived]
: 過濾掉已封存的筆記本。for notebook in active_notebooks: notebook_list_item(notebook)
: 我們遍歷獲取的筆記本列表,對每一個筆記本呼叫 notebook_list_item
函式,將其顯示在頁面上。st.button("開啟", key=...)
: 建立一個按鈕。Streamlit 的按鈕很有趣:當按鈕被點擊時,st.button(...)
會回傳 True
,並且 Streamlit 會從頭到尾重新執行整個 Python 腳本。key
參數確保每個筆記本的按鈕都有唯一的識別碼。st.session_state["current_notebook_id"] = notebook.id
: st.session_state
是 Streamlit 用來在多次重新執行之間保存狀態的地方。這裡我們將被點擊的筆記本的 ID 存起來。st.rerun()
: 強制 Streamlit 立即重新執行腳本。UI 還需要提供讓使用者輸入資訊的方式,例如建立新筆記本時輸入名稱和描述。
# pages/2_📒_Notebooks.py (簡化片段)
# ... (前面顯示列表的程式碼) ...
# 使用 st.expander 建立一個可展開/收合的區塊
with st.expander("➕ **新增筆記本**"):
# 使用 st.text_input 建立一個文字輸入框,用於輸入名稱
new_notebook_title = st.text_input("新筆記本名稱")
# 使用 st.text_area 建立一個多行文字輸入區,用於輸入描述
new_notebook_description = st.text_area(
"描述",
placeholder="解釋這個筆記本的用途..."
)
# 建立一個 "Create" 按鈕
if st.button("建立新筆記本", icon="➕"):
# 如果按鈕被點擊
if new_notebook_title: # 簡單檢查名稱是否為空
# 1. 建立一個 Notebook 物件 (使用 ObjectModel)
notebook = Notebook(
name=new_notebook_title, description=new_notebook_description
)
# 2. 呼叫 save() 方法將其儲存到資料庫 (使用 ObjectModel)
notebook.save()
st.toast("筆記本建立成功", icon="📒") # 顯示一個短暫的提示訊息
st.rerun() # 重新執行頁面以更新列表
else:
st.warning("請輸入筆記本名稱") # 如果名稱為空,顯示警告
# ... (顯示封存筆記本的程式碼) ...
程式碼解釋:
st.expander(...)
: 建立一個可以點擊展開或收合的區塊。st.text_input(...)
: 顯示一個單行文字輸入框,並將使用者輸入的內容賦值給 new_notebook_title
變數。st.text_area(...)
: 顯示一個多行文字區域,用於輸入描述。st.button("建立新筆記本", ...)
: 建立建立按鈕。if st.button(...)
: 當使用者點擊「建立新筆記本」按鈕時,這段程式碼會被執行。notebook = Notebook(...)
: 根據使用者輸入的標題和描述,建立一個 Notebook
物件實例(這是 物件模型 (ObjectModel) 的應用)。notebook.save()
: 呼叫 Notebook
物件的 save()
方法,將新筆記本的資料儲存到資料庫(這也是 物件模型 (ObjectModel) 的功能)。st.toast(...)
: 在畫面上顯示一個短暫的、會自動消失的訊息。st.rerun()
: 再次重新執行頁面腳本,這樣剛剛建立的新筆記本就會出現在列表中。當使用者點擊某個筆記本的「開啟」按鈕時,我們需要顯示該筆記本的詳細內容,而不是列表。這通常透過 st.session_state
來控制。
# pages/2_📒_Notebooks.py (簡化片段)
# ... (匯入 和 setup_page) ...
def notebook_page(current_notebook: Notebook):
"""顯示單一筆記本內容的函式 (頁面片段)"""
st.header(current_notebook.name) # 顯示筆記本名稱
st.write(current_notebook.description) # 顯示描述
# 顯示返回按鈕
if st.button("返回列表", icon="🔙"):
st.session_state["current_notebook_id"] = None # 清除狀態
st.rerun() # 返回列表頁面
# 顯示該筆記本的來源和筆記 (使用 source_card 和 note_card 元件)
st.subheader("來源")
for source in current_notebook.sources:
source_card(source=source, notebook_id=current_notebook.id)
st.subheader("筆記")
for note in current_notebook.notes:
note_card(note=note, notebook_id=current_notebook.id)
# ... 可能還有聊天側邊欄等其他元件 ...
# --- 主邏輯 ---
# 檢查 session_state 中是否儲存了當前要顯示的筆記本 ID
if "current_notebook_id" not in st.session_state:
st.session_state["current_notebook_id"] = None
if st.session_state["current_notebook_id"]:
# 如果有 ID,則從資料庫讀取該 Notebook 物件
current_notebook: Notebook = Notebook.get(st.session_state["current_notebook_id"])
if not current_notebook:
st.error("找不到筆記本")
st.session_state["current_notebook_id"] = None # 清除無效 ID
st.rerun()
else:
# 呼叫 notebook_page 函式來顯示單一筆記本的內容
notebook_page(current_notebook)
st.stop() # 停止執行,避免顯示後面的列表程式碼
else:
# 如果沒有 current_notebook_id,則顯示筆記本列表
st.title("📒 我的筆記本")
# ... (顯示 "新增筆記本" expander 的程式碼) ...
# ... (顯示筆記本列表的程式碼) ...
程式碼解釋:
st.session_state
中是否有 "current_notebook_id"
。"current_notebook_id"
存在且有效,就使用 Notebook.get()
從資料庫讀取該筆記本,然後呼叫 notebook_page()
函式來顯示其詳細內容。st.stop()
會阻止腳本繼續執行下去(即不會顯示列表)。"current_notebook_id"
不存在或為 None
,則執行 else
區塊的程式碼,顯示筆記本列表和新增表單。notebook_page()
函式: 這個函式負責呈現單一筆記本的介面,包括標題、描述、來源列表(使用 source_card
元件)和筆記列表(使用 note_card
元件)。它還提供了一個「返回列表」按鈕,該按鈕會將 st.session_state["current_notebook_id"]
設為 None
並 rerun
,從而回到列表視圖。這個例子展示了 Streamlit 如何透過簡單的 Python 指令建立 UI、如何與後端(物件模型)互動來讀寫資料、以及如何使用 st.session_state
來管理簡單的頁面狀態和導覽。
理解 Streamlit 的基本運作方式有助於更好地使用它。
Streamlit 最核心的概念是它的重新執行 (rerun) 模型。當使用者與任何 widget(按鈕、滑塊、文字輸入等)互動時,Streamlit 會從頭到尾重新執行整個 Python 腳本。
這聽起來可能效率不高,但 Streamlit 在內部做了很多優化:
open-notebook
在很多地方利用了這個特性,例如在 模型管理器 (ModelManager) 中快取載入的模型。st.session_state
)由於每次互動都會重新執行腳本,普通的 Python 變數無法在多次執行之間保持其值。st.session_state
就是為了解決這個問題而設計的。它是一個類似字典的物件,您可以在其中儲存需要在重新執行之間保留的資訊。
我們在上面的例子中看到了如何使用 st.session_state["current_notebook_id"]
來追蹤使用者想要查看的筆記本。open-notebook
在許多地方使用 st.session_state
來儲存:
active_session
)。messages
)。context_config
)。search_results
)。當使用者在 Streamlit UI 中執行一個操作(例如點擊「儲存」按鈕)時,通常會發生以下情況:
流程解釋:
if st.button(...)
),條件為真。Notebook
物件的 save()
方法)。st.session_state
或準備新的 UI 元素(例如 st.toast
訊息)。open-notebook
的 UI 相關程式碼主要分佈在:
app_home.py
: 應用程式的主入口點。pages/
: 這個目錄下的每個 .py
檔案都對應側邊欄的一個頁面。Streamlit 會自動處理檔案名到頁面標題的轉換(例如 2_📒_Notebooks.py
變成 "📒 Notebooks")。pages/components/
: 存放可重複使用的 UI 元件,例如 note_panel.py
, source_panel.py
。這些元件通常是接收資料並使用 Streamlit widgets 來顯示/編輯資料的函式。pages/stream_app/
: 存放與特定 Streamlit 頁面邏輯緊密相關的輔助函式或元件,例如 chat.py
(處理聊天介面邏輯), note.py
(處理筆記相關的 UI 函式如 note_card
, add_note
), utils.py
(通用輔助函式如 setup_page
, check_models
)。這種結構有助於保持 UI 程式碼的組織性和可維護性。
在本章中,我們探索了 open-notebook
的使用者介面,以及它是如何使用 Streamlit 函式庫建立的。
st.title
, st.write
)、獲取使用者輸入 (st.text_input
, st.button
) 以及如何觸發後端邏輯 (物件模型 (ObjectModel) 的 get_all
, save
方法)。st.session_state
進行狀態管理。現在我們有了一個可以讓使用者瀏覽資料、輸入內容並與 AI 模型互動的介面。但是,當使用者透過 UI 新增了一個來源(例如一個網址或一個文件)後,應用程式需要對這個來源進行處理,例如提取文字內容、產生摘要、計算嵌入向量等。這個處理過程可能包含多個步驟。
在下一章,我們將深入探討 內容處理流程 (Content Processing Graph),了解 open-notebook
如何定義和執行這些多步驟的內容處理任務。