Spaces:
Sleeping
Sleeping
import os | |
os.environ["STREAMLIT_GLOBAL_CONFIG"] = "/data/.streamlit/config.toml" | |
import uuid | |
import random | |
import urllib.parse # To parse URL parameters | |
import streamlit as st | |
import duckdb | |
# Database file path | |
DB_PATH = '/data/steampolis.duckdb' | |
# Initialize database tables if they don't exist | |
def initialize_database(): | |
try: | |
init_con = duckdb.connect(database=DB_PATH, read_only=False) | |
init_con.execute(""" | |
CREATE TABLE IF NOT EXISTS topics ( | |
id TEXT PRIMARY KEY, | |
name TEXT NOT NULL, | |
description TEXT, | |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
) | |
""") | |
init_con.execute(""" | |
CREATE TABLE IF NOT EXISTS users ( | |
id TEXT PRIMARY KEY, | |
username TEXT NOT NULL UNIQUE, | |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
) | |
""") | |
init_con.execute(""" | |
CREATE TABLE IF NOT EXISTS comments ( | |
id TEXT PRIMARY KEY, | |
topic_id TEXT NOT NULL, | |
user_id TEXT NOT NULL, | |
content TEXT NOT NULL, | |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
FOREIGN KEY (topic_id) REFERENCES topics(id), | |
FOREIGN KEY (user_id) REFERENCES users(id) | |
) | |
""") | |
init_con.execute(""" | |
CREATE TABLE IF NOT EXISTS votes ( | |
id TEXT PRIMARY KEY, | |
user_id TEXT NOT NULL, | |
comment_id TEXT NOT NULL, | |
vote_type TEXT NOT NULL CHECK (vote_type IN ('agree', 'disagree', 'neutral')), | |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
FOREIGN KEY (user_id) REFERENCES users(id), | |
FOREIGN KEY (comment_id) REFERENCES comments(id), | |
UNIQUE (user_id, comment_id) | |
) | |
""") | |
init_con.execute(""" | |
CREATE TABLE IF NOT EXISTS user_comment_collections ( | |
id TEXT PRIMARY KEY, | |
user_id TEXT NOT NULL, | |
comment_id TEXT NOT NULL, | |
collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
FOREIGN KEY (user_id) REFERENCES users(id), | |
FOREIGN KEY (comment_id) REFERENCES comments(id), | |
UNIQUE (user_id, comment_id) | |
) | |
""") | |
init_con.execute(""" | |
CREATE TABLE IF NOT EXISTS user_progress ( | |
id TEXT PRIMARY KEY, | |
user_id TEXT NOT NULL, | |
topic_id TEXT NOT NULL, | |
last_comment_id_viewed TEXT, | |
last_viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
FOREIGN KEY (user_id) REFERENCES users(id), | |
FOREIGN KEY (topic_id) REFERENCES topics(id), | |
FOREIGN KEY (last_comment_id_viewed) REFERENCES comments(id), | |
UNIQUE (user_id, topic_id) | |
) | |
""") | |
# Create system user if it doesn't exist | |
try: | |
init_con.execute(""" | |
INSERT INTO users (id, username) | |
VALUES ('system', 'System') | |
ON CONFLICT (id) DO NOTHING | |
""") | |
except Exception as e: | |
print(f"Warning: Could not create system user: {e}") | |
except Exception as e: | |
st.error(f"Database initialization failed: {e}") | |
finally: | |
if 'init_con' in locals() and init_con: | |
init_con.close() | |
# Helper function to get a random unvoted comment | |
def get_random_unvoted_comment(user_id, topic_id): | |
local_con = None | |
try: | |
local_con = duckdb.connect(database=DB_PATH, read_only=False) | |
# First, check if there are any comments at all in the topic | |
comment_count = local_con.execute(""" | |
SELECT COUNT(*) FROM comments WHERE topic_id = ? | |
""", [topic_id]).fetchone()[0] | |
if comment_count == 0: | |
return None, "No comments in this topic yet." | |
# Attempt to get a random comment that the user has NOT voted on | |
result = local_con.execute(""" | |
SELECT c.id, c.content | |
FROM comments c | |
WHERE c.topic_id = ? | |
AND NOT EXISTS ( | |
SELECT 1 FROM votes v | |
WHERE v.comment_id = c.id AND v.user_id = ? | |
) | |
ORDER BY RANDOM() | |
LIMIT 1 | |
""", [topic_id, user_id]).fetchone() | |
if result: | |
# Found an unvoted comment | |
return result[0], result[1] | |
else: | |
# No unvoted comments found for this user in this topic | |
return None, "No new thoughts for now..." | |
except Exception as e: | |
st.error(f"Error getting random unvoted comment: {e}") | |
return None, f"Error loading comments: {str(e)}" | |
finally: | |
if local_con: | |
local_con.close() | |
# Helper function to find or create a user | |
def find_or_create_user(username): | |
local_con = None | |
try: | |
local_con = duckdb.connect(database=DB_PATH, read_only=False) | |
user_result = local_con.execute("SELECT id FROM users WHERE username = ?", [username]).fetchone() | |
if user_result: | |
return user_result[0] | |
else: | |
user_id = str(uuid.uuid4()) | |
local_con.execute("INSERT INTO users (id, username) VALUES (?, ?)", [user_id, username]) | |
return user_id | |
except Exception as e: | |
st.error(f"Error finding or creating user: {e}") | |
return None | |
finally: | |
if local_con: | |
local_con.close() | |
# Helper function to update user progress | |
def update_user_progress(user_id, topic_id, comment_id): | |
local_con = None | |
try: | |
local_con = duckdb.connect(database=DB_PATH, read_only=False) | |
progress_id = str(uuid.uuid4()) | |
local_con.execute(""" | |
INSERT INTO user_progress (id, user_id, topic_id, last_comment_id_viewed) VALUES (?, ?, ?, ?) | |
ON CONFLICT (user_id, topic_id) DO UPDATE SET | |
last_comment_id_viewed = EXCLUDED.last_comment_id_viewed | |
""", [progress_id, user_id, topic_id, comment_id]) | |
except Exception as e: | |
st.error(f"Error updating user progress: {e}") | |
finally: | |
if local_con: | |
local_con.close() | |
# Helper function to handle comment submission UI and logic | |
def share_wisdom(prompt, allow_skip=False): | |
st.markdown(prompt) | |
new_comment_text = st.text_area(f"Your Insight {'that different from others above (Empty to skip)' if allow_skip else ''}", key="new_comment_input") | |
if st.button("Share Your Wisdom"): | |
if new_comment_text: | |
user_id = find_or_create_user(user_email) # Ensure user exists | |
if user_id: | |
local_con = None | |
try: | |
local_con = duckdb.connect(database=DB_PATH, read_only=False) | |
comment_id = str(uuid.uuid4()) | |
local_con.execute(""" | |
INSERT INTO comments (id, topic_id, user_id, content) | |
VALUES (?, ?, ?, ?) | |
""", [comment_id, topic_id, user_id, new_comment_text]) | |
# Append new comment to history | |
st.session_state.comment_history += f"\n\n㪠{new_comment_text}" | |
# Get next comment (could be the one just submitted) | |
next_comment_id, next_comment_content = get_random_unvoted_comment(user_id, topic_id) | |
st.session_state.current_comment_id = next_comment_id | |
st.session_state.current_comment_content = next_comment_content | |
# Update progress | |
update_user_progress(user_id, topic_id, next_comment_id) | |
st.session_state.new_comment_input = "" # Clear input box | |
st.rerun() # Rerun to update UI | |
except Exception as e: | |
st.error(f"Error sharing information: {e}") | |
finally: | |
if local_con: | |
local_con.close() | |
else: | |
st.error("Could not find or create user.") | |
elif allow_skip: | |
return | |
else: | |
st.warning("Please enter your thought.") | |
# --- Page Functions --- | |
def home_page(): | |
st.title("Welcome to SteamPolis") | |
st.markdown("Choose an option:") | |
if st.button("Create New Topic"): | |
st.session_state.page = 'create_topic' | |
st.rerun() | |
st.markdown("---") | |
st.markdown("Or join an existing topic:") | |
topic_input = st.text_input("Enter Topic ID or URL") | |
if st.button("Join Topic"): | |
topic_id = topic_input.strip() | |
if topic_id.startswith('http'): # Handle full URL | |
parsed_url = urllib.parse.urlparse(topic_id) | |
query_params = urllib.parse.parse_qs(parsed_url.query) | |
topic_id = query_params.get('topic', [None])[0] | |
if topic_id: | |
st.session_state.page = 'view_topic' | |
st.session_state.current_topic_id = topic_id | |
# Attempt to load email from session state (mimics browser state) | |
# If email exists, handle email submission logic immediately on view page load | |
st.rerun() | |
else: | |
st.warning("Please enter a valid Topic ID or URL.") | |
def create_topic_page(): | |
st.title("Create a New Topic") | |
new_topic_name = st.text_input("Topic Name (Imagine you are the king, how would you share your concern)") | |
new_topic_description = st.text_area('Description (Begin with "I want to figure out...", imagine you are the king, what would you want to know)', height=150) | |
new_topic_seed_comments = st.text_area("Initial Comments (separate by new line, imagine there are civilians what will they answer)", height=200) | |
creator_email = st.text_input("Enter your Email (required for creation)") | |
if st.button("Create Topic"): | |
if not creator_email: | |
st.error("Email is required to create a topic.") | |
return | |
topic_id = str(uuid.uuid4())[:8] | |
user_id = find_or_create_user(creator_email) | |
if user_id: | |
local_con = None | |
try: | |
local_con = duckdb.connect(database=DB_PATH, read_only=False) | |
local_con.execute("INSERT INTO topics (id, name, description) VALUES (?, ?, ?)", [topic_id, new_topic_name, new_topic_description]) | |
seed_comments = [c.strip() for c in new_topic_seed_comments.split('\n') if c.strip()] | |
for comment in seed_comments: | |
comment_id = str(uuid.uuid4()) | |
local_con.execute("INSERT INTO comments (id, topic_id, user_id, content) VALUES (?, ?, ?, ?)", | |
[comment_id, topic_id, 'system', comment]) | |
# Get the first comment to display after creation | |
comment_to_display_id, comment_to_display_content = get_random_unvoted_comment(user_id, topic_id) | |
# Set initial progress for creator | |
update_user_progress(user_id, topic_id, comment_to_display_id) | |
st.session_state.page = 'view_topic' | |
st.session_state.current_topic_id = topic_id | |
st.session_state.user_email = creator_email # Store email in session state | |
st.session_state.current_comment_id = comment_to_display_id | |
st.session_state.current_comment_content = comment_to_display_content | |
st.session_state.comment_history = "" | |
st.success(f"Topic '{new_topic_name}' created!") | |
st.rerun() | |
except Exception as e: | |
st.error(f"Error creating topic: {e}") | |
finally: | |
if local_con: | |
local_con.close() | |
else: | |
st.error("Could not find or create user.") | |
if st.button("Back to Home"): | |
st.session_state.page = 'home' | |
st.rerun() | |
def view_topic_page(): | |
topic_id = st.session_state.get('current_topic_id') | |
user_email = st.session_state.get('user_email', '') | |
current_comment_id = st.session_state.get('current_comment_id') | |
current_comment_content = st.session_state.get('current_comment_content', "Loading comments...") | |
comment_history = st.session_state.get('comment_history', "") | |
if not topic_id: | |
st.warning("No topic selected. Returning to home.") | |
st.session_state.page = 'home' | |
st.rerun() | |
return | |
local_con = None | |
topic_name = "Loading..." | |
topic_description = "Loading..." | |
try: | |
local_con = duckdb.connect(database=DB_PATH, read_only=True) | |
topic_result = local_con.execute("SELECT name, description FROM topics WHERE id = ?", [topic_id]).fetchone() | |
if topic_result: | |
topic_name, topic_description = topic_result | |
else: | |
st.error(f"Topic ID '{topic_id}' not found.") | |
st.session_state.page = 'home' | |
st.rerun() | |
return | |
except Exception as e: | |
st.error(f"Error loading topic details: {e}") | |
if local_con: | |
local_con.close() | |
st.session_state.page = 'home' | |
st.rerun() | |
return | |
finally: | |
if local_con: | |
local_con.close() | |
# Include functional information | |
st.markdown(f"**Quest Scroll ID:** `{topic_id}`") | |
# Construct shareable link using current app URL | |
app_url = st.query_params.get('base', ['http://localhost:8501/'])[0] # Get base URL if available | |
shareable_link = f"{app_url}?topic={topic_id}" if app_url else f"?topic={topic_id}" | |
st.markdown(f"**Shareable Scroll Link:** `{shareable_link}`") | |
st.title("Seeker Quest") | |
# Check if user email is available in session state. | |
# user_email is already retrieved from st.session_state at the start of view_topic_page. | |
if user_email: | |
# Get the user ID. find_or_create_user handles the DB connection internally. | |
user_id = find_or_create_user(user_email) | |
if user_id: | |
# Check if user has any progress recorded for this specific topic. | |
# This indicates they have viewed comments or interacted before. | |
local_con = None | |
progress_exists = False | |
try: | |
local_con = duckdb.connect(database=DB_PATH, read_only=True) | |
# Query the user_progress table for a record matching user_id and topic_id | |
result = local_con.execute(""" | |
SELECT 1 FROM user_progress | |
WHERE user_id = ? AND topic_id = ? | |
LIMIT 1 | |
""", [user_id, topic_id]).fetchone() | |
progress_exists = result is not None | |
except Exception as e: | |
# Log error but don't stop the app. Assume no progress on error. | |
st.error(f"Error checking user progress for greeting: {e}") | |
# progress_exists remains False | |
finally: | |
if local_con: | |
local_con.close() | |
# Display the appropriate greeting based on progress | |
if progress_exists: | |
# Acknowledge return and remind of quest | |
st.markdown("Welcome back, Seeker. Your journey through the whispers of Aethelgard continues.") | |
st.markdown(f"You pause to recall the heart of the Emperor's concern regarding **{topic_name}**: `{topic_description}`.") | |
# Introduce the next comment | |
st.markdown("As you press onward, you encounter another soul willing to share their thoughts on this vital matter.") | |
else: | |
# Introduce the setting and the Emperor's concern | |
st.markdown("Welcome, Seeker, to the ancient Kingdom of Aethelgard, a realm of digital whispers and forgotten wisdom.") | |
st.markdown("For centuries, Aethelgard has stood, preserving the echoes of an age long past. But now, a matter of great weight troubles the Emperor's thoughts.") | |
st.markdown(f"The Emperor seeks clarity on a crucial topic: **`{topic_name}`**.") | |
# Explain the quest and the user's role | |
st.markdown("You, among a select few, have been summoned for a vital quest: to traverse the kingdom, gather insights, and illuminate this matter for the throne.") | |
st.markdown(f"At a recent royal gathering, the Emperor revealed the heart of their concern, the very essence of your mission: `{topic_description}`") | |
# Transition to the task | |
st.markdown("Your journey begins now. The path leads to the first village, where the voices of the realm await your ear.") | |
# --- Email Prompt --- | |
if not user_email: | |
st.subheader("Enter your Email to view comments and progress") | |
view_user_email_input = st.text_input("Your Email", key="view_email_input") | |
if st.button("Submit Email", key="submit_view_email"): | |
if view_user_email_input: | |
st.session_state.user_email = view_user_email_input | |
user_id = find_or_create_user(view_user_email_input) | |
if user_id: | |
comment_to_display_id, comment_to_display_content = get_random_unvoted_comment(user_id, topic_id) | |
st.session_state.current_comment_id = comment_to_display_id | |
st.session_state.current_comment_content = comment_to_display_content | |
update_user_progress(user_id, topic_id, comment_to_display_id) | |
st.session_state.comment_history = "" # Reset history on new email submission | |
st.rerun() | |
else: | |
st.error("Could not find or create user with that email.") | |
else: | |
st.warning("Please enter your email.") | |
return # Stop rendering the rest until email is submitted | |
# --- Comment Display and Voting --- | |
# Define introductory phrases for encountering a new perspective | |
intro_phrases = [ | |
"A new whisper reaches your ear", | |
"You ponder a fresh perspective", | |
"Another voice shares their view", | |
"A thought emerges from the crowd", | |
"The wind carries a new idea", | |
"Someone offers an insight", | |
"You overhear a comment", | |
"A different angle appears", | |
"The village elder shares", | |
"A traveler murmurs", | |
] | |
# Randomly select a phrase | |
random_phrase = random.choice(intro_phrases) | |
if current_comment_id: # Only show voting if there's a comment to vote on | |
# Display comment history and the current comment with the random intro | |
st.markdown(f"{comment_history}\n\n[Collected new insight, {random_phrase}]:\n* {current_comment_content}") | |
# Handle vote logic | |
def handle_vote(vote_type, comment_id, topic_id, user_id): | |
local_con = None | |
try: | |
local_con = duckdb.connect(database=DB_PATH, read_only=False) | |
vote_id = str(uuid.uuid4()) | |
local_con.execute(""" | |
INSERT INTO votes (id, user_id, comment_id, vote_type) | |
VALUES (?, ?, ?, ?) | |
""", [vote_id, user_id, comment_id, vote_type]) | |
# Append voted comment to history | |
vote_text = "π" if vote_type == "agree" else "π" if vote_type == "disagree" else "π" | |
st.session_state.comment_history += f"\n\n{vote_text} {current_comment_content}" | |
# Check vote count and trigger special event | |
# Initialize vote_count if it doesn't exist | |
if 'vote_count' not in st.session_state: | |
st.session_state.vote_count = 0 | |
st.session_state.vote_count += 1 | |
# Check if it's time for a potential special event (every 5 votes) | |
if st.session_state.vote_count % 5 == 0: | |
st.session_state.vote_count = 0 | |
# 30% chance to trigger the special sharing event | |
if random.random() < 0.3: | |
prompts = [ | |
"An elder approaches you, seeking your perspective on the Emperor's concern. What wisdom do you share?", | |
"A letter arrives from the Emperor's office, requesting your personal insight on the matter. What counsel do you offer?", | |
"As you walk through the streets, people gather, eager to hear your thoughts on the Emperor's dilemma. What advice do you give?" | |
] | |
share_wisdom(random.choice(prompts), allow_skip=True) | |
# Get next comment | |
next_comment_id, next_comment_content = get_random_unvoted_comment(user_id, topic_id) | |
st.session_state.current_comment_id = next_comment_id | |
st.session_state.current_comment_content = next_comment_content | |
# Update progress | |
update_user_progress(user_id, topic_id, next_comment_id) | |
st.rerun() # Rerun to update UI | |
except Exception as e: | |
st.error(f"Error processing vote: {e}") | |
finally: | |
if local_con: | |
local_con.close() | |
col1, col2, col3, col4 = st.columns(4) | |
user_id = find_or_create_user(user_email) # Ensure user exists | |
col1.markdown("*Personally I...*") | |
if col2.button("Agree"): | |
handle_vote("agree", current_comment_id, topic_id, user_id) | |
if col3.button("Neutral"): | |
handle_vote("neutral", current_comment_id, topic_id, user_id) | |
if col4.button("Disagree"): | |
handle_vote("disagree", current_comment_id, topic_id, user_id) | |
else: | |
st.info("No more comments to vote on in this topic." if "No more comments" in current_comment_content else current_comment_content) | |
st.markdown("") | |
# --- Comment Submission --- | |
with st.expander("Offer Your Counsel to the Emperor", expanded=False): | |
share_wisdom("Having heard the thoughts of others, what wisdom do you wish to share regarding the Emperor's concern?") | |
st.markdown("---") | |
if st.button("Pack all insights and Return to Capital"): | |
st.session_state.page = 'home' | |
st.rerun() | |
# Initialize session state for navigation and data | |
if 'page' not in st.session_state: | |
st.session_state.page = 'home' | |
if 'current_topic_id' not in st.session_state: | |
st.session_state.current_topic_id = None | |
if 'user_email' not in st.session_state: | |
st.session_state.user_email = '' # Mimics browser state | |
if 'current_comment_id' not in st.session_state: | |
st.session_state.current_comment_id = None | |
if 'current_comment_content' not in st.session_state: | |
st.session_state.current_comment_content = "Loading comments..." | |
if 'comment_history' not in st.session_state: | |
st.session_state.comment_history = "" | |
# Initialize the database on first run | |
initialize_database() | |
# Handle initial load from URL query parameters | |
query_params = st.query_params | |
if 'topic' in query_params and st.session_state.page == 'home': | |
topic_id_from_url = query_params['topic'] | |
st.session_state.page = 'view_topic' | |
st.session_state.current_topic_id = topic_id_from_url | |
# The view_topic_page will handle loading user/comment based on session_state.user_email | |
st.query_params = {} # Clear query params after processing | |
st.rerun() | |
# Render the appropriate page based on session state | |
if st.session_state.page == 'home': | |
home_page() | |
elif st.session_state.page == 'create_topic': | |
create_topic_page() | |
elif st.session_state.page == 'view_topic': | |
view_topic_page() |