opusdev/vector-similarity-api
1
1import React, { useState, useEffect, useRef } from "react";2import { Heart, MessageCircle, Share2, Bookmark } from "lucide-react";3 4// Language Selector Component5function LanguageSelector({ selectedLanguage, onLanguageChange }) {6 return (7 <div style={{ display: "flex", gap: "10px", marginBottom: "20px", justifyContent: "center" }}>8 <button9 onClick={() => onLanguageChange("english")}10 style={{11 padding: "10px 24px",12 background: selectedLanguage === "english" ? "#fff" : "#1a1a1a",13 color: selectedLanguage === "english" ? "#000" : "#888",14 border: `1px solid ${selectedLanguage === "english" ? "#fff" : "#222"}`,15 borderRadius: "8px",16 cursor: "pointer",17 fontWeight: 600,18 fontSize: "14px",19 transition: "all 0.2s ease",20 textTransform: "uppercase",21 letterSpacing: "0.5px",22 }}23 >24 english25 </button>26 <button27 onClick={() => onLanguageChange("hindi")}28 style={{29 padding: "10px 24px",30 background: selectedLanguage === "hindi" ? "#fff" : "#1a1a1a",31 color: selectedLanguage === "hindi" ? "#000" : "#888",32 border: `1px solid ${selectedLanguage === "hindi" ? "#fff" : "#222"}`,33 borderRadius: "8px",34 cursor: "pointer",35 fontWeight: 600,36 fontSize: "14px",37 transition: "all 0.2s ease",38 textTransform: "uppercase",39 letterSpacing: "0.5px",40 }}41 >42 हिंदी 43 </button>44 </div>45 );46}47 48// Create Post Form Component49function CreatePostForm({ onSubmit, onCancel, loading }) {50 const [name, setName] = useState("");51 const [caption, setCaption] = useState("");52 const [mediaUrl, setMediaUrl] = useState("");53 const [mediaType, setMediaType] = useState("image");54 const [category, setCategory] = useState("tech");55 const [errors, setErrors] = useState({});56 57 const handleSubmit = (e) => {58 e.preventDefault();59 const newErrors = {};60 61 if (!name.trim()) newErrors.name = "Name is required";62 if (!caption.trim()) newErrors.caption = "Caption is required";63 if (!mediaUrl.trim()) newErrors.media_url = "Media URL is required";64 if (!mediaType.trim()) newErrors.media_type = "Media type is required";65 if (!category.trim()) newErrors.category = "Category is required";66 67 if (Object.keys(newErrors).length > 0) {68 setErrors(newErrors);69 return;70 }71 72 onSubmit({73 name: name.trim(),74 caption: caption.trim(),75 media_url: mediaUrl.trim(),76 media_type: mediaType.trim(),77 category: category.trim(),78 });79 };80 81 return (82 <div83 style={{84 position: "fixed",85 top: 0,86 left: 0,87 right: 0,88 bottom: 0,89 background: "rgba(0,0,0,0.9)",90 backdropFilter: "blur(8px)",91 display: "flex",92 alignItems: "center",93 justifyContent: "center",94 zIndex: 1001,95 padding: "20px",96 }}97 onClick={onCancel}98 >99 <div100 style={{101 background: "#0a0a0a",102 border: "1px solid #1a1a1a",103 borderRadius: "12px",104 padding: "32px",105 maxWidth: 500,106 width: "100%",107 boxShadow: "0 20px 50px rgba(0, 0, 0, 0.6)",108 maxHeight: "90vh",109 overflowY: "auto",110 }}111 onClick={(e) => e.stopPropagation()}112 >113 <h2114 style={{115 fontSize: "24px",116 fontWeight: 600,117 marginBottom: "8px",118 background: "linear-gradient(to bottom,#10775f,#139c77,#33b89b)",119 WebkitBackgroundClip: "text",120 WebkitTextFillColor: "transparent",121 }}122 >123 Create New Post124 </h2>125 <p style={{ color: "#666", fontSize: "13px", marginBottom: "24px" }}>126 All fields are required. Your post will be indexed with AI embeddings.127 </p>128 129 <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "16px" }}>130 {/* Name Field */}131 <div>132 <label style={{ display: "block", fontSize: "12px", color: "#aaa", marginBottom: "6px", fontWeight: 600 }}>133 Author Name / Post Title *134 </label>135 <input136 type="text"137 value={name}138 onChange={(e) => {139 setName(e.target.value);140 if (errors.name) setErrors({ ...errors, name: "" });141 }}142 placeholder="e.g., John Doe"143 style={{144 width: "100%",145 padding: "10px 12px",146 background: "#111",147 border: `1px solid ${errors.name ? "#dc2626" : "#222"}`,148 borderRadius: "8px",149 color: "#fff",150 fontSize: "13px",151 boxSizing: "border-box",152 }}153 />154 {errors.name && <div style={{ fontSize: "11px", color: "#f87171", marginTop: "4px" }}>{errors.name}</div>}155 </div>156 157 {/* Caption Field */}158 <div>159 <label style={{ display: "block", fontSize: "12px", color: "#aaa", marginBottom: "6px", fontWeight: 600 }}>160 Caption / Description *161 </label>162 <textarea163 value={caption}164 onChange={(e) => {165 setCaption(e.target.value);166 if (errors.caption) setErrors({ ...errors, caption: "" });167 }}168 placeholder="Describe your post..."169 style={{170 width: "100%",171 padding: "10px 12px",172 background: "#111",173 border: `1px solid ${errors.caption ? "#dc2626" : "#222"}`,174 borderRadius: "8px",175 color: "#fff",176 fontSize: "13px",177 fontFamily: "system-ui,sans-serif",178 resize: "vertical",179 minHeight: "80px",180 boxSizing: "border-box",181 }}182 />183 {errors.caption && <div style={{ fontSize: "11px", color: "#f87171", marginTop: "4px" }}>{errors.caption}</div>}184 </div>185 186 {/* Media URL Field */}187 <div>188 <label style={{ display: "block", fontSize: "12px", color: "#aaa", marginBottom: "6px", fontWeight: 600 }}>189 Media URL (Image) *190 </label>191 <input192 type="url"193 value={mediaUrl}194 onChange={(e) => {195 setMediaUrl(e.target.value);196 if (errors.media_url) setErrors({ ...errors, media_url: "" });197 }}198 placeholder="https://example.com/image.jpg"199 style={{200 width: "100%",201 padding: "10px 12px",202 background: "#111",203 border: `1px solid ${errors.media_url ? "#dc2626" : "#222"}`,204 borderRadius: "8px",205 color: "#fff",206 fontSize: "13px",207 boxSizing: "border-box",208 }}209 />210 {errors.media_url && <div style={{ fontSize: "11px", color: "#f87171", marginTop: "4px" }}>{errors.media_url}</div>}211 {mediaUrl && (212 <img213 src={mediaUrl}214 alt="Preview"215 style={{216 marginTop: "8px",217 maxWidth: "100%",218 height: "auto",219 maxHeight: "150px",220 borderRadius: "6px",221 objectFit: "cover",222 }}223 onError={(e) => {224 e.target.style.display = "none";225 }}226 />227 )}228 </div>229 230 {/* Media Type Field */}231 <div>232 <label style={{ display: "block", fontSize: "12px", color: "#aaa", marginBottom: "6px", fontWeight: 600 }}>233 Media Type *234 </label>235 <select236 value={mediaType}237 onChange={(e) => {238 setMediaType(e.target.value);239 if (errors.media_type) setErrors({ ...errors, media_type: "" });240 }}241 style={{242 width: "100%",243 padding: "10px 12px",244 background: "#111",245 border: `1px solid ${errors.media_type ? "#dc2626" : "#222"}`,246 borderRadius: "8px",247 color: "#fff",248 fontSize: "13px",249 boxSizing: "border-box",250 }}251 >252 <option value="image">Image</option>253 <option value="video">Video</option>254 </select>255 {errors.media_type && <div style={{ fontSize: "11px", color: "#f87171", marginTop: "4px" }}>{errors.media_type}</div>}256 </div>257 258 {/* Category Field */}259 <div>260 <label style={{ display: "block", fontSize: "12px", color: "#aaa", marginBottom: "6px", fontWeight: 600 }}>261 Category *262 </label>263 <select264 value={category}265 onChange={(e) => {266 setCategory(e.target.value);267 if (errors.category) setErrors({ ...errors, category: "" });268 }}269 style={{270 width: "100%",271 padding: "10px 12px",272 background: "#111",273 border: `1px solid ${errors.category ? "#dc2626" : "#222"}`,274 borderRadius: "8px",275 color: "#fff",276 fontSize: "13px",277 boxSizing: "border-box",278 }}279 >280 <option value="tech">Tech</option>281 <option value="ai">AI</option>282 <option value="healthcare">Healthcare</option>283 <option value="food">Food</option>284 <option value="art">Art</option>285 <option value="education">Education</option>286 <option value="travel">Travel</option>287 <option value="music">Music</option>288 <option value="sports">Sports</option>289 <option value="web3">Web3</option>290 <option value="finance">Finance</option>291 <option value="movies">Movies</option>292 <option value="nature">Nature</option>293 <option value="socialmedia">Social Media</option>294 <option value="stocks">Stocks</option>295 <option value="vehicles">Vehicles</option>296 <option value="cafes">Cafes</option>297 </select>298 {errors.category && <div style={{ fontSize: "11px", color: "#f87171", marginTop: "4px" }}>{errors.category}</div>}299 </div>300 301 {/* Buttons */}302 <div style={{ display: "flex", gap: "12px", marginTop: "12px" }}>303 <button304 type="submit"305 disabled={loading}306 style={{307 flex: 1,308 padding: "12px 16px",309 background: loading ? "#333" : "#fff",310 color: loading ? "#666" : "#000",311 border: "none",312 borderRadius: "8px",313 cursor: loading ? "not-allowed" : "pointer",314 fontWeight: 700,315 fontSize: "14px",316 transition: "all 0.2s ease",317 }}318 >319 {loading ? "Creating..." : "Create Post"}320 </button>321 <button322 type="button"323 onClick={onCancel}324 disabled={loading}325 style={{326 flex: 1,327 padding: "12px 16px",328 background: "#1a1a1a",329 color: "#888",330 border: "1px solid #222",331 borderRadius: "8px",332 cursor: loading ? "not-allowed" : "pointer",333 fontWeight: 600,334 fontSize: "14px",335 }}336 >337 Cancel338 </button>339 </div>340 </form>341 </div>342 </div>343 );344}345 346function CategorySelector({ categories, onApply, onSkip }) {347 const [selected, setSelected] = useState([]);348 const handleSelect = (cat) => {349 setSelected((prev) =>350 prev.includes(cat)351 ? prev.filter((c) => c !== cat)352 : prev.length < 3353 ? [...prev, cat]354 : prev355 );356 };357 return (358 <div style={{359 position: 'fixed',360 top: 0,361 left: 0,362 right: 0,363 bottom: 0,364 background: 'rgba(0,0,0,0.85)',365 backdropFilter: 'blur(8px)',366 display: 'flex',367 alignItems: 'center',368 justifyContent: 'center',369 zIndex: 1000,370 padding: '20px'371 }}>372 <div style={{373 background: '#0a0a0a',374 border: '1px solid #1a1a1a',375 borderRadius: 12,376 padding: '40px 32px',377 maxWidth: 500,378 width: '100%',379 boxShadow: '0 20px 50px rgba(0, 0, 0, 0.6)',380 textAlign: 'center',381 animation: 'fadeInUp 0.6s ease-out'382 }}>383 <style>{`384 @keyframes fadeInUp {385 from { opacity: 0; transform: translateY(15px); }386 to { opacity: 1; transform: translateY(0); }387 }388 `}</style>389 <h2 style={{ 390 fontSize: '28px',391 fontWeight: 400,392 marginBottom: '8px',393 background: 'linear-gradient(to bottom,#10775f,#139c77,#33b89b)',394 WebkitBackgroundClip: 'text',395 WebkitTextFillColor: 'transparent',396 letterSpacing: '-0.5px'397 }}>Select Interests</h2>398 <p style={{ 399 color: '#666', 400 fontSize: '13px', 401 marginBottom: 32,402 lineHeight: '1.5'403 }}>The algorithm needs a starting point. Choose 3 categories to begin your personalized feed.</p>404 405 <div style={{ 406 display: 'flex', 407 flexWrap: 'wrap', 408 gap: 8, 409 justifyContent: 'center', 410 marginBottom: 36 411 }}>412 {categories.map((cat) => (413 <button414 key={cat}415 onClick={() => handleSelect(cat)}416 style={{417 background: selected.includes(cat) ? '#33b89b' : '#111',418 color: selected.includes(cat) ? '#000' : '#888',419 border: '1px solid',420 borderColor: selected.includes(cat) ? '#33b89b' : '#222',421 borderRadius: 6,422 padding: '7px 14px',423 fontWeight: 600,424 cursor: 'pointer',425 fontSize: '12px',426 transition: 'all 0.2s ease',427 textTransform: 'uppercase',428 letterSpacing: '0.5px'429 }}430 >431 {cat}432 </button>433 ))}434 </div>435 436 <div style={{ display: 'flex', gap: 12, justifyContent: 'center' }}>437 <button438 onClick={() => onApply(selected)}439 disabled={selected.length !== 3}440 style={{441 background: selected.length === 3 ? '#fff' : '#1a1a1a',442 color: selected.length === 3 ? '#000' : '#444',443 border: 'none',444 borderRadius: 8,445 padding: '12px 32px',446 fontWeight: 700,447 fontSize: '14px',448 cursor: selected.length === 3 ? 'pointer' : 'not-allowed',449 transition: 'all 0.3s ease',450 flex: 1,451 textTransform: 'uppercase'452 }}453 >Initialize Feed</button>454 <button455 onClick={onSkip}456 style={{457 background: 'transparent',458 color: '#444',459 border: '1px solid #222',460 borderRadius: 8,461 padding: '12px 24px',462 fontWeight: 600,463 fontSize: '14px',464 cursor: 'pointer',465 transition: 'all 0.3s ease',466 textTransform: 'uppercase'467 }}468 onMouseOver={(e) => e.target.style.color = '#fff'}469 onMouseOut={(e) => e.target.style.color = '#444'}470 >Skip</button>471 </div>472 </div>473 </div>474 );475}476 477// List of all categories478const ALL_CATEGORIES = [479 'nature', 'tech', 'healthcare', 'food', 'art', 'education', 'travel', 'music', 'sports', 'ai', 'web3', 'socialmedia', 'finance', 'movies', 'stocks', 'vehicles', 'cafes'480];481 482const API_BASE = window.location.hostname === "localhost" ? "http://localhost:7860" : "";483const USER_ID = "default_user";484 485const SOURCE_BADGE = {486 query: { bg: "#1a4d2e", color: "#4ade80", text: "Query Match" },487 interest: { bg: "#1e3a8a", color: "#60a5fa", text: "For You" },488 random: { bg: "#4c1d95", color: "#a78bfa", text: "Discover" },489};490const RANK_COLOR = {491 primary: "#f59e0b", // PRIMARY tier (>= 15 likes)492 secondary: "#60a5fa", // SECONDARY tier (>= 10 likes)493 tertiary: "#a78bfa",494 quaternary: "#34d399",495 quinary: "#f472b6",496 senary: "#38bdf8",497 septenary: "#fb923c",498 octonary: "#4ade80",499 PRIMARY: "#f59e0b", // Tier-based constants500 SECONDARY: "#60a5fa",501 TERTIARY: "#a78bfa",502};503const CATEGORY_COLOR = {504 tech: "#3b82f6",505 ai: "#8b5cf6",506 healthcare: "#10b981",507 web3: "#f59e0b",508 socialmedia: "#ec4899",509 food: "#f97316",510 sports: "#06b6d4",511 finance: "#84cc16",512 movies: "#ef4444",513 music: "#a855f7",514 education: "#14b8a6",515 travel: "#6366f1",516 art: "#f43f5e",517 nature: "#22c55e",518 unknown: "#6b7280",519 liked: "#f59e0b",520 stocks: "#84cc16",521 crypto: "#f59e0b",522};523const getCatColor = (c) => CATEGORY_COLOR[c?.toLowerCase()] || "#6b7280";524const getBadge = (s) => SOURCE_BADGE[s] || SOURCE_BADGE.query;525 526const BENTO_PATTERN = [527 { col: 1, row: 2 },528 { col: 2, row: 1 },529 { col: 1, row: 1 },530 { col: 1, row: 1 },531 { col: 1, row: 1 },532 { col: 2, row: 2 },533 { col: 1, row: 1 },534 { col: 1, row: 2 },535 { col: 2, row: 1 },536 { col: 1, row: 1 },537 { col: 1, row: 1 },538 { col: 2, row: 1 },539];540 541function BentoCard({ post, span, visible, animDelay }) {542 const cc = getCatColor(post.category),543 isBig = span.col === 2 && span.row === 2,544 isWide = span.col === 2 && span.row === 1;545 return (546 <div547 style={{548 gridColumn: `span ${span.col}`,549 gridRow: `span ${span.row}`,550 background: post.media_url551 ? `#0e0e0e url("${post.media_url}") center/cover no-repeat`552 : "#0e0e0e",553 border: "1px solid #1e1e1e",554 borderRadius: "10px",555 overflow: "hidden",556 position: "relative",557 opacity: visible ? 1 : 0,558 transform: visible ? "scale(1)" : "scale(0.95)",559 transition: `opacity 0.45s ease ${animDelay}ms, transform 0.45s ease ${animDelay}ms`,560 display: "flex",561 flexDirection: "column",562 justifyContent: "flex-end",563 }}564 >565 <div566 style={{567 position: "absolute",568 inset: 0,569 zIndex: 1,570 background: post.media_url571 ? "linear-gradient(to bottom,transparent 20%,rgba(0,0,0,.55) 60%,rgba(0,0,0,.92) 100%)"572 : "linear-gradient(to bottom,#111 0%,#0a0a0a 100%)",573 }}574 />575 <div576 style={{577 padding: isBig ? "11px 12px" : isWide ? "8px 10px" : "7px 9px",578 position: "relative",579 zIndex: 2,580 }}581 >582 {post.category && post.category !== "unknown" && (583 <div584 style={{585 display: "inline-block",586 background: cc + "28",587 color: cc,588 fontSize: "8px",589 fontWeight: 800,590 letterSpacing: "0.6px",591 padding: "2px 6px",592 borderRadius: "4px",593 marginBottom: "4px",594 textTransform: "uppercase",595 }}596 >597 {post.category}598 </div>599 )}600 <div601 style={{602 fontWeight: 700,603 fontSize: isBig ? "12px" : "11px",604 color: "#f0f0f0",605 lineHeight: 1.3,606 marginBottom: "3px",607 overflow: "hidden",608 display: "-webkit-box",609 WebkitLineClamp: isBig ? 2 : 1,610 WebkitBoxOrient: "vertical",611 }}612 >613 {post.name}614 </div>615 <div616 style={{617 fontSize: "9px",618 color: post.media_url ? "rgba(255,255,255,.55)" : "#555",619 lineHeight: 1.35,620 overflow: "hidden",621 display: "-webkit-box",622 WebkitLineClamp: isBig ? 3 : 2,623 WebkitBoxOrient: "vertical",624 }}625 >626 {post.caption}627 </div>628 </div>629 </div>630 );631}632 633function BentoGrid({ posts, visible }) {634 const [cv, setCv] = useState(false);635 useEffect(() => {636 if (posts.length > 0) {637 const t = setTimeout(() => setCv(true), 80);638 return () => clearTimeout(t);639 }640 }, [posts]);641 if (!posts.length)642 return (643 <div644 style={{645 display: "grid",646 gridTemplateColumns: "repeat(4,1fr)",647 gridAutoRows: "60px",648 gap: "5px",649 marginBottom: "28px",650 }}651 >652 {BENTO_PATTERN.map((s, i) => (653 <div654 key={i}655 style={{656 gridColumn: `span ${s.col}`,657 gridRow: `span ${s.row}`,658 background: "#0e0e0e",659 borderRadius: "10px",660 animation: `pulse 1.8s ease-in-out ${i * 60}ms infinite`,661 }}662 />663 ))}664 <style>{`@keyframes pulse{0%,100%{opacity:.25}50%{opacity:.5}}`}</style>665 </div>666 );667 return (668 <div669 style={{670 display: "grid",671 gridTemplateColumns: "repeat(4,1fr)",672 gridAutoRows: "60px",673 gap: "5px",674 marginBottom: "28px",675 opacity: visible ? 1 : 0,676 transform: visible ? "none" : "translateY(8px)",677 transition: "opacity .5s ease,transform .5s ease",678 }}679 >680 {posts.slice(0, 12).map((p, i) => (681 <BentoCard682 key={p.post_id}683 post={p}684 span={BENTO_PATTERN[i] || { col: 1, row: 1 }}685 visible={cv}686 animDelay={i * 35}687 />688 ))}689 </div>690 );691}692 693function InterestPills({ rankedInterests }) {694 if (!rankedInterests?.length) return null;695 return (696 <div697 style={{698 display: "flex",699 gap: "6px",700 flexWrap: "wrap",701 marginBottom: "16px",702 alignItems: "center",703 }}704 >705 <span706 style={{707 fontSize: "9px",708 color: "#444",709 letterSpacing: "1px",710 textTransform: "uppercase",711 marginRight: "2px",712 }}713 >714 Your interests715 </span>716 {rankedInterests.map((r) => {717 const rc = RANK_COLOR[r.rank] || "#6b7280",718 cc = getCatColor(r.category);719 720 // Color code by tier721 const tierColor = r.tier === "PRIMARY" 722 ? "#f59e0b" // Amber for PRIMARY 723 : r.tier === "SECONDARY" 724 ? "#60a5fa" // Blue for SECONDARY725 : rc; // Default color for others726 727 return (728 <div729 key={`${r.category}-${r.tier || r.rank}`}730 style={{731 display: "flex",732 alignItems: "center",733 gap: "4px",734 background: tierColor + "12",735 border: `1px solid ${tierColor}33`,736 borderRadius: "20px",737 padding: "3px 9px",738 }}739 >740 <span741 style={{742 fontSize: "8px",743 color: tierColor,744 fontWeight: 800,745 textTransform: "uppercase",746 letterSpacing: "0.4px",747 }}748 >749 {r.tier || r.rank}750 </span>751 <span style={{ fontSize: "9px", color: cc, fontWeight: 700 }}>752 {r.category}753 </span>754 <span style={{ fontSize: "8px", color: "#555" }}>{r.count}♥</span>755 </div>756 );757 })}758 </div>759 );760}761 762// rag component 763function RAGResponse({ data, loading }) {764 if (!data && !loading) return null;765 766 if (loading) {767 return (768 <div style={{ textAlign: "center", padding: "40px" }}>769 <div770 style={{771 display: "inline-block",772 width: "24px",773 height: "24px",774 border: "2px solid #222",775 borderTopColor: "#fff",776 borderRadius: "50%",777 animation: "spin .8s linear infinite",778 }}779 />780 <style>{`@keyframes spin{to{transform:rotate(360deg)}}`}</style>781 </div>782 );783 }784 785 if (!data) return null;786 787 return (788 <div789 style={{790 background: "#0a0a0a",791 border: "1px solid #1a1a1a",792 borderRadius: "12px",793 padding: "24px",794 marginBottom: "28px",795 }}796 >797{/* img loader for rag */}798 {data.featured_image?.url && (799 <div style={{ marginBottom: "24px" }}>800 <img801 src={data.featured_image.url}802 alt="Featured"803 style={{804 width: "100%",805 height: "auto",806 maxHeight: "300px",807 objectFit: "cover",808 borderRadius: "10px",809 marginBottom: "8px",810 }}811 onError={(e) => {812 e.target.style.display = "none";813 }}814 />815 <div style={{ fontSize: "11px", color: "#666" }}>816 From: <strong>{data.featured_image.source}</strong>817 </div>818 </div>819 )}820 821{/* summary */}822 {data.summary && (823 <div style={{ marginBottom: "24px" }}>824 <h4 style={{ marginBottom: "10px", fontSize: "14px", color: "#aaa" }}>825 Summary826 </h4>827 <p828 style={{829 margin: 0,830 color: "#ccc",831 lineHeight: "1.6",832 fontSize: "13px",833 }}834 >835 {data.summary}836 </p>837 </div>838 )}839 {data.key_insights && data.key_insights.length > 0 && (840 <div style={{ marginBottom: "24px" }}>841 <h4 style={{ marginBottom: "14px", fontSize: "14px", color: "#aaa" }}>842 Key Insights ({data.key_insights.length})843 </h4>844 <div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>845 {data.key_insights.map((insight, idx) => (846 <div847 key={idx}848 style={{849 background: "#111",850 padding: "14px",851 borderRadius: "8px",852 border: "1px solid #1a1a1a",853 }}854 >855 <div style={{ display: "flex", gap: "10px" }}>856 <div857 style={{858 background: "#60a5fa",859 color: "#000",860 width: "24px",861 height: "24px",862 borderRadius: "50%",863 display: "flex",864 alignItems: "center",865 justifyContent: "center",866 fontWeight: 700,867 fontSize: "12px",868 flexShrink: 0,869 }}870 >871 {insight.rank}872 </div>873 <div style={{ flex: 1 }}>874 <div875 style={{876 fontWeight: 600,877 fontSize: "13px",878 marginBottom: "6px",879 color: "#fff",880 }}881 >882 {insight.point}883 </div>884 <div885 style={{886 fontSize: "12px",887 color: "#aaa",888 lineHeight: "1.5",889 marginBottom: "6px",890 }}891 >892 {insight.explanation}893 </div>894 <div895 style={{896 fontSize: "11px",897 color: "#666",898 }}899 >900 Source: <strong>{insight.post_reference}</strong>901 </div>902 </div>903 </div>904 </div>905 ))}906 </div>907 </div>908 )}909 910 {data.ai_perspective && (911 <div style={{ marginBottom: "24px" }}>912 <h4 style={{ marginBottom: "12px", fontSize: "14px", color: "#aaa" }}>913 AI's Perspective914 </h4>915 <div916 style={{917 background: "#111",918 padding: "16px",919 borderRadius: "8px",920 border: "1px solid #1a1a1a",921 borderLeft: "3px solid #8b5cf6",922 }}923 >924 <p925 style={{926 margin: 0,927 color: "#ccc",928 lineHeight: "1.6",929 fontSize: "13px",930 whiteSpace: "pre-wrap",931 }}932 >933 {data.ai_perspective}934 </p>935 </div>936 </div>937 )}938 939 {/* Source Posts Grid */}940 {data.source_posts && data.source_posts.length > 0 && (941 <div>942 <h4 style={{ marginBottom: "14px", fontSize: "14px", color: "#aaa" }}>943 Source Posts ({data.source_posts.length})944 </h4>945 <div946 style={{947 display: "grid",948 gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",949 gap: "12px",950 }}951 >952 {data.source_posts.map((post, idx) => (953 <div954 key={idx}955 style={{956 background: "#111",957 border: "1px solid #1a1a1a",958 borderRadius: "8px",959 overflow: "hidden",960 }}961 >962 {post.media_url && (963 <img964 src={post.media_url}965 alt={post.name}966 style={{967 width: "100%",968 height: "120px",969 objectFit: "cover",970 background: "#0a0a0a",971 }}972 onError={(e) => {973 e.target.style.display = "none";974 }}975 />976 )}977 <div style={{ padding: "12px" }}>978 <div979 style={{980 fontWeight: 600,981 fontSize: "12px",982 marginBottom: "6px",983 color: "#fff",984 }}985 >986 {post.name}987 </div>988 <div989 style={{990 fontSize: "11px",991 color: "#aaa",992 lineHeight: "1.4",993 marginBottom: "8px",994 overflow: "hidden",995 display: "-webkit-box",996 WebkitLineClamp: 2,997 WebkitBoxOrient: "vertical",998 }}999 >1000 {post.caption}1001 </div>1002 <div style={{ fontSize: "10px", color: "#666" }}>1003 Score: <strong>{post.similarity_score}</strong>1004 </div>1005 </div>1006 </div>1007 ))}1008 </div>1009 </div>1010 )}1011 {data.metadata && (1012 <div1013 style={{1014 marginTop: "20px",1015 padding: "12px",1016 background: "#000",1017 border: "1px solid #1a1a1a",1018 borderRadius: "8px",1019 fontSize: "10px",1020 color: "#666",1021 }}1022 >1023 <div>Total posts analyzed: {data.metadata.posts_analyzed}</div>1024 <div>Top similarity: {data.metadata.top_similarity}</div>1025 </div>1026 )}1027 </div>1028 );1029}1030 1031export default function App() {1032 const [showCatSelector, setShowCatSelector] = useState(() => {1033 return sessionStorage.getItem('first_search_done') !== 'true';1034 });1035 1036 const handleApplyInitialCats = async (selectedCats) => {1037 sessionStorage.setItem('first_search_done', 'true');1038 setShowCatSelector(false);1039 try {1040 const res = await fetch(API_BASE + '/posts');1041 const data = await res.json();1042 const allPosts = data.posts || [];1043 const normalizedCats = selectedCats.map((c) => c.toLowerCase().trim());1044 const selectedCatSet = new Set(normalizedCats);1045 const postsByCategory = new Map();1046 1047 allPosts.forEach((post) => {1048 const pCat = (post.category || "").toLowerCase().trim();1049 if (!postsByCategory.has(pCat)) {1050 postsByCategory.set(pCat, []);1051 }1052 postsByCategory.get(pCat).push(post);1053 });1054 1055 let catPosts = [];1056 normalizedCats.forEach((normCat) => {1057 const filtered = (postsByCategory.get(normCat) || []).map((p) => ({1058 ...p,1059 source: "query",1060 }));1061 const shuffled = [...filtered].sort(() => Math.random() - 0.5);1062 catPosts = catPosts.concat(shuffled.slice(0, 3)); // 3 per cat to be safe1063 });1064 1065 const otherPosts = allPosts1066 .filter((p) => {1067 const pCat = (p.category || "").toLowerCase().trim();1068 return !selectedCatSet.has(pCat);1069 })1070 .map((p) => ({ ...p, source: "random" }))1071 .sort(() => Math.random() - 0.5);1072 1073 const randomPad = otherPosts.slice(0, 4);1074 let postsToShow = [...catPosts, ...randomPad];1075 1076 // Final fallback: if zero posts, just grab random ones1077 if (postsToShow.length === 0) {1078 postsToShow = allPosts.slice(0, 10).map(p => ({...p, source: 'random'}));1079 }1080 1081 postsToShow.sort(() => Math.random() - 0.5);1082 setResults(postsToShow);1083 setBreakdown({1084 budget: { query: catPosts.length, interest: 0, random: randomPad.length },1085 query_based: catPosts.length,1086 interest_based: 0,1087 random: randomPad.length,1088 total: postsToShow.length,1089 ranked_interests: [],1090 });1091 setShowBento(false);1092 } catch (err) {1093 console.error('[init cats]', err);1094 setError('Failed to load initial posts');1095 }1096 };1097 1098 const handleSkipInitialCats = () => {1099 sessionStorage.setItem('first_search_done', 'true');1100 setShowCatSelector(false);1101 };1102 useEffect(() => {1103 if (sessionStorage.getItem('first_search_done') !== 'true') {1104 setShowCatSelector(true);1105 }1106 }, []);1107 const [query, setQuery] = useState("");1108 const [results, setResults] = useState([]);1109 const [loading, setLoading] = useState(false);1110 const [error, setError] = useState("");1111 const [selectedLanguage, setSelectedLanguage] = useState("english"); // NEW: Language selection1112 const [breakdown, setBreakdown] = useState(null);1113 const [bentoPosts, setBentoPosts] = useState([]);1114 const [bentoLoaded, setBentoLoaded] = useState(false);1115 const [showBento, setShowBento] = useState(true);1116 const [likeEvents, setLikeEvents] = useState([]); // Now stores all engagement events1117 const leRef = useRef([]);1118 const [likedIds, setLikedIds] = useState(new Set());1119 const liRef = useRef(new Set());1120 const [watchTimers, setWatchTimers] = useState({}); // Track watch timers for videos1121 leRef.current = likeEvents;1122 liRef.current = likedIds;1123 1124 // Comment modal state1125 const [commentModal, setCommentModal] = useState({ open: false, postId: null });1126 const [commentText, setCommentText] = useState("");1127 1128 // Saved posts state1129 const [savedPosts, setSavedPosts] = useState(new Set());1130 const savedRef = useRef(new Set());1131 1132 // Shared posts state1133 const [sharedPosts, setSharedPosts] = useState(new Set());1134 const sharedRef = useRef(new Set());1135 const [sharingPost, setSharingPost] = useState(null);1136 1137 // Comments state - maps post_id to array of comments1138 const [postComments, setPostComments] = useState({});1139 1140 // Save state being processed1141 const [savingPost, setSavingPost] = useState(null);1142 1143 // Create post state1144 const [showCreateModal, setShowCreateModal] = useState(false);1145 const [creatingPost, setCreatingPost] = useState(false);1146 1147 const searchInputRef = useRef(null);1148 1149// rag handlerss 1150 const [question, setQuestion] = useState("");1151 const [ragData, setRagData] = useState(null);1152 const [ragLoading, setRagLoading] = useState(false);1153 1154 function handleRagKeyPress(e) {1155 if (e.key === "Enter") {1156 handleRagAsk();1157 }1158 }1159 1160 async function handleRagAsk() {1161 if (!question.trim()) {1162 setError("Please enter a question");1163 return;1164 }1165 1166 setRagLoading(true);1167 setRagData(null);1168 setError("");1169 1170 try {1171 const res = await fetch(`${API_BASE}/rag`, {1172 method: "POST",1173 headers: { "Content-Type": "application/json" },1174 body: JSON.stringify({1175 question: question.trim(),1176 limit: 5,1177 min_score: 0.1,1178 user_id: USER_ID,1179 }),1180 });1181 1182 if (!res.ok) {1183 throw new Error(`HTTP ${res.status}: ${await res.text()}`);1184 }1185 1186 const data = await res.json();1187 console.log("[RAG Response]", data);1188 1189 if (data.key_insights) {1190 setRagData(data);1191 } else if (data.answer) {1192 setRagData({1193 summary: data.answer,1194 key_insights: [],1195 ai_perspective:1196 "No AI perspective available with this response format.",1197 source_posts: data.sources || [],1198 featured_image: null,1199 });1200 }