File size: 5,395 Bytes
454408d
 
 
 
 
e8b0e61
454408d
 
 
 
 
e8b0e61
273764c
454408d
e8b0e61
 
454408d
 
e8b0e61
454408d
e8b0e61
 
454408d
 
e8b0e61
 
 
 
 
 
 
454408d
e8b0e61
454408d
e8b0e61
 
 
 
 
454408d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8b0e61
 
 
 
 
 
 
 
 
454408d
 
e8b0e61
 
 
 
 
 
 
454408d
e8b0e61
 
 
 
 
 
 
 
454408d
 
 
 
 
 
 
e8b0e61
454408d
 
e8b0e61
454408d
e8b0e61
454408d
 
e8b0e61
 
454408d
 
 
 
 
 
 
 
 
e8b0e61
454408d
 
 
 
 
 
 
 
e8b0e61
454408d
 
e8b0e61
454408d
 
 
e8b0e61
454408d
 
 
e8b0e61
454408d
 
 
 
 
 
 
 
 
 
 
e8b0e61
454408d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8b0e61
454408d
 
 
 
e8b0e61
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import streamlit as st
import networkx as nx
from pyvis.network import Network
import streamlit.components.v1 as components
from supabase import create_client, Client
from sentence_transformers import SentenceTransformer # NEW IMPORT
import json
import datetime
import math
import os

st.set_page_config(layout="wide", page_title="Semantic Topic Search")

# --- CONFIGURATION ---
MAX_NODES = 500
TIME_SCALE = 1 / 3600
LABELS_FILE = "topic_labels.json"

# --- LOAD RESOURCES ---
@st.cache_resource
def load_resources():
    # Load Supabase
    url = os.environ["SUPABASE_URL"]
    key = os.environ["SUPABASE_KEY"]
    sb = create_client(url, key)
    
    # Load Embedding Model (Cached so it only loads once)
    # This runs locally on your machine/server
    model = SentenceTransformer('all-MiniLM-L6-v2')
    
    return sb, model

supabase, model = load_resources()

# Load Labels
labels_map = {}
if os.path.exists(LABELS_FILE):
    with open(LABELS_FILE, 'r') as f:
        labels_map = json.load(f)

# --- HELPER: Time Color ---
def get_time_color(timestamp, max_ts, min_ts):
    if not timestamp: return "#888888"
    if max_ts == min_ts: return "#FF0000"
    age = (max_ts - timestamp) * TIME_SCALE
    max_age = (max_ts - min_ts) * TIME_SCALE
    try:
        decay = math.log(age + 1) / math.log(max_age + 1)
        ratio = max(0.0, min(1.0, 1.0 - decay))
    except ValueError:
        ratio = 0.5
    r = int(ratio * 255)
    g = int((1 - ratio) * 255)
    b = int((1 - ratio) * 255)
    return f"#{r:02x}{g:02x}{b:02x}"

# --- UI ---
st.title("🧠 Semantic Topic Search")
st.markdown("Search based on **meaning**, not just keywords.")

st.sidebar.header("Search Parameters")
search_query = st.sidebar.text_input("Concept Search", placeholder="e.g. 'Space exploration costs'")
limit = st.sidebar.slider("Max Posts", 50, 500, 200)
threshold = st.sidebar.slider("Similarity Threshold", 0.0, 1.0, 0.3, help="0.0 = Match anything, 1.0 = Exact match only")
node_spacing = st.sidebar.slider("Node Spacing", 50, 500, 250)

if search_query:
    with st.spinner(f"Vectorizing query & searching database..."):
        
        # 1. Vectorize the User's Query
        # We convert the text input into a list of 384 floats
        query_vector = model.encode(search_query).tolist()

        # 2. Call the RPC function in Supabase
        try:
            response = supabase.rpc(
                "match_posts", 
                {
                    "query_embedding": query_vector,
                    "match_threshold": threshold,
                    "match_count": limit
                }
            ).execute()
            
            data = response.data
        except Exception as e:
            st.error(f"Database Error: {e}")
            st.stop()

    if not data:
        st.warning("No posts found with that semantic meaning. Try lowering the threshold.")
        st.stop()

    st.success(f"Found {len(data)} semantically related posts.")

    # 3. Build Graph (Logic is identical to previous script)
    G = nx.Graph()
    timestamps = [row['post_timestamp'] for row in data if row['post_timestamp']]
    min_ts = min(timestamps) if timestamps else 0
    max_ts = max(timestamps) if timestamps else 1

    for row in data:
        user_id = row['author_user_id']
        topic_id = row['cluster_id']
        ts = row['post_timestamp'] or 0
        
        if not user_id: continue
        
        u_node = f"User_{user_id}"
        if topic_id is not None and topic_id != 0:
            raw_t_id = f"Topic_{topic_id}"
            t_label = labels_map.get(raw_t_id, f"Topic {topic_id}")
            t_node = raw_t_id
        else:
            t_node = "Topic_Unknown"
            t_label = "Uncategorized"

        if u_node not in G:
            G.add_node(u_node, label=f"User {user_id}", title=f"User ID: {user_id}", color="#97C2FC", size=10)
        
        if t_node not in G:
            G.add_node(t_node, label=t_label, title=t_label, color="#FB7E81", size=25)

        if G.has_edge(u_node, t_node):
            G[u_node][t_node]['weight'] += 1
            if ts > G[u_node][t_node]['ts']: G[u_node][t_node]['ts'] = ts
        else:
            G.add_edge(u_node, t_node, weight=1, ts=ts)

    # 4. Render PyVis
    nt = Network(height="700px", width="100%", bgcolor="#222222", font_color="white")
    
    for node_id in G.nodes:
        attr = G.nodes[node_id]
        nt.add_node(node_id, label=attr['label'], title=attr['title'], color=attr['color'], size=attr['size'])

    for u, v, d in G.edges(data=True):
        weight = d['weight']
        ts = d['ts']
        color = get_time_color(ts, max_ts, min_ts)
        date_str = datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d') if ts > 0 else "Unknown"
        title = f"{weight} posts related to search\nLast Active: {date_str}"
        nt.add_edge(u, v, value=weight, title=title, color=color)

    nt.set_options(f"""
    var options = {{
      "physics": {{
        "barnesHut": {{
          "gravitationalConstant": -8000,
          "springLength": {node_spacing},
          "springConstant": 0.04,
          "damping": 0.2
        }},
        "minVelocity": 0.75
      }}
    }}
    """)
    
    html_data = nt.generate_html()
    components.html(html_data, height=720, scrolling=False)

else:
    st.info("👈 Enter a concept (e.g., 'artificial intelligence dangers') to search semantically.")