Ship native iOS app, Wiki/TTS backend, and skeleton loading UX.

Replace the WebView shell with SwiftUI screens, add account-scoped Wiki and TTS APIs with adaptive review and photo scan support, and keep web/iOS pages usable while data loads asynchronously.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-08 15:13:59 +08:00
parent db5465c646
commit 05e173b293
115 changed files with 12709 additions and 1212 deletions
+198
View File
@@ -21,6 +21,49 @@ def run_migrations() -> None:
conn.execute(
text("ALTER TABLE words ADD COLUMN train_count INTEGER NOT NULL DEFAULT 0")
)
if "difficulty" not in cols:
conn.execute(
text("ALTER TABLE words ADD COLUMN difficulty DOUBLE NOT NULL DEFAULT 5.0")
)
if "stability_hours" not in cols:
conn.execute(
text(
"ALTER TABLE words ADD COLUMN stability_hours "
"DOUBLE NOT NULL DEFAULT 24.0"
)
)
if "next_review_at" not in cols:
conn.execute(text("ALTER TABLE words ADD COLUMN next_review_at VARCHAR(32)"))
if "error_bank_member" not in cols:
conn.execute(
text(
"ALTER TABLE words ADD COLUMN error_bank_member "
"INTEGER NOT NULL DEFAULT 0"
)
)
if "error_reinforce_streak" not in cols:
conn.execute(
text(
"ALTER TABLE words ADD COLUMN error_reinforce_streak "
"INTEGER NOT NULL DEFAULT 0"
)
)
if "error_bank_entered_at" not in cols:
conn.execute(
text("ALTER TABLE words ADD COLUMN error_bank_entered_at VARCHAR(32)")
)
conn.execute(
text(
"UPDATE words SET error_bank_member = 1 "
"WHERE wrong_count > 0 AND error_bank_member = 0"
)
)
conn.execute(
text(
"UPDATE words SET error_bank_member = 1 "
"WHERE error_reinforce_streak >= 2 AND error_bank_member = 0"
)
)
if insp.has_table("quiz_records"):
cols = {c["name"] for c in insp.get_columns("quiz_records")}
@@ -36,6 +79,161 @@ def run_migrations() -> None:
cols = {c["name"] for c in insp.get_columns("user_settings")}
if "active_book_id" not in cols:
conn.execute(text("ALTER TABLE user_settings ADD COLUMN active_book_id INTEGER"))
if "ai_wiki_enabled" not in cols:
conn.execute(
text(
"ALTER TABLE user_settings ADD COLUMN ai_wiki_enabled "
"INTEGER NOT NULL DEFAULT 0"
)
)
if "ai_wiki_auto_organize" not in cols:
conn.execute(
text(
"ALTER TABLE user_settings ADD COLUMN ai_wiki_auto_organize "
"INTEGER NOT NULL DEFAULT 0"
)
)
if "error_clear_correct_count" not in cols:
conn.execute(
text(
"ALTER TABLE user_settings ADD COLUMN error_clear_correct_count "
"INTEGER NOT NULL DEFAULT 2"
)
)
if insp.has_table("user_book_settings"):
ubs_cols = {c["name"] for c in insp.get_columns("user_book_settings")}
if "error_clear_correct_count" not in ubs_cols:
conn.execute(
text(
"ALTER TABLE user_book_settings ADD COLUMN error_clear_correct_count "
"INTEGER NOT NULL DEFAULT 2"
)
)
if not insp.has_table("wiki_sources"):
conn.execute(
text(
"""
CREATE TABLE wiki_sources (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
user_id INTEGER NOT NULL,
word_id INTEGER NULL,
source_key VARCHAR(96) NOT NULL,
source_type VARCHAR(32) NOT NULL,
payload TEXT NOT NULL,
created_at VARCHAR(32) NOT NULL,
CONSTRAINT uq_wiki_source_user_key UNIQUE (user_id, source_key),
FOREIGN KEY(user_id) REFERENCES users (id),
FOREIGN KEY(word_id) REFERENCES words (id)
)
"""
)
)
conn.execute(text("CREATE INDEX ix_wiki_sources_user_id ON wiki_sources (user_id)"))
conn.execute(text("CREATE INDEX ix_wiki_sources_word_id ON wiki_sources (word_id)"))
if not insp.has_table("wiki_pages"):
conn.execute(
text(
"""
CREATE TABLE wiki_pages (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
user_id INTEGER NOT NULL,
word_id INTEGER NULL,
slug VARCHAR(160) NOT NULL,
title VARCHAR(500) NOT NULL,
category VARCHAR(32) NOT NULL,
summary TEXT NOT NULL,
content TEXT NOT NULL,
source_count INTEGER NOT NULL DEFAULT 0,
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL,
CONSTRAINT uq_wiki_page_user_slug UNIQUE (user_id, slug),
FOREIGN KEY(user_id) REFERENCES users (id),
FOREIGN KEY(word_id) REFERENCES words (id)
)
"""
)
)
conn.execute(text("CREATE INDEX ix_wiki_pages_user_id ON wiki_pages (user_id)"))
conn.execute(text("CREATE INDEX ix_wiki_pages_word_id ON wiki_pages (word_id)"))
if not insp.has_table("wiki_links"):
conn.execute(
text(
"""
CREATE TABLE wiki_links (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
user_id INTEGER NOT NULL,
source_page_id INTEGER NOT NULL,
target_page_id INTEGER NOT NULL,
relation VARCHAR(32) NOT NULL,
created_at VARCHAR(32) NOT NULL,
CONSTRAINT uq_wiki_link_edge UNIQUE (
user_id, source_page_id, target_page_id, relation
),
FOREIGN KEY(user_id) REFERENCES users (id),
FOREIGN KEY(source_page_id) REFERENCES wiki_pages (id),
FOREIGN KEY(target_page_id) REFERENCES wiki_pages (id)
)
"""
)
)
conn.execute(text("CREATE INDEX ix_wiki_links_user_id ON wiki_links (user_id)"))
conn.execute(
text("CREATE INDEX ix_wiki_links_source_page_id ON wiki_links (source_page_id)")
)
conn.execute(
text("CREATE INDEX ix_wiki_links_target_page_id ON wiki_links (target_page_id)")
)
if not insp.has_table("wiki_logs"):
conn.execute(
text(
"""
CREATE TABLE wiki_logs (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
user_id INTEGER NOT NULL,
action VARCHAR(32) NOT NULL,
page_slug VARCHAR(160) NULL,
detail TEXT NOT NULL,
created_at VARCHAR(32) NOT NULL,
FOREIGN KEY(user_id) REFERENCES users (id)
)
"""
)
)
conn.execute(text("CREATE INDEX ix_wiki_logs_user_id ON wiki_logs (user_id)"))
if not insp.has_table("wiki_llm_usage"):
conn.execute(
text(
"""
CREATE TABLE wiki_llm_usage (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
user_id INTEGER NOT NULL,
usage_date VARCHAR(10) NOT NULL,
call_count INTEGER NOT NULL DEFAULT 0,
CONSTRAINT uq_wiki_llm_usage_day UNIQUE (user_id, usage_date),
FOREIGN KEY(user_id) REFERENCES users (id)
)
"""
)
)
conn.execute(text("CREATE INDEX ix_wiki_llm_usage_user_id ON wiki_llm_usage (user_id)"))
if insp.has_table("wiki_pages"):
conn.execute(
text(
"""
UPDATE wiki_pages SET
title = REPLACE(title, 'WordLoop LLM Wiki', 'WordLoop Wiki'),
content = REPLACE(content, 'WordLoop LLM Wiki', 'WordLoop Wiki')
WHERE title LIKE '%LLM Wiki%' OR content LIKE '%LLM Wiki%'
"""
)
)
if not insp.has_table("user_book_settings"):
conn.execute(