Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion sql/core/spark_ideas.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ CREATE TABLE IF NOT EXISTS core.spark_ideas (
complexity TEXT,
estimated_impact TEXT,
similar_agents JSONB,
user_reaction TEXT,
popularity_score INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_spark_ideas_user_reaction
CHECK (user_reaction IS NULL OR user_reaction IN ('like', 'dislike'))
);

CREATE INDEX IF NOT EXISTS idx_spark_ideas_company_id
Expand Down
78 changes: 73 additions & 5 deletions tavro_api/api/routers/spark.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
import os
import re
from pathlib import Path
from typing import Any, AsyncGenerator
from typing import Any, AsyncGenerator, Literal

logger = logging.getLogger(__name__)

CURRENT_YEAR = datetime.datetime.now().year

import httpx
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sqlalchemy import text
Expand Down Expand Up @@ -62,6 +62,18 @@ class SparkIdea(BaseModel):
complexity: str
estimated_impact: str
similar_agents: list[SparkSimilarAgent]
user_reaction: Literal["like", "dislike"] | None = None
popularity_score: int = 0


class SparkReactionRequest(BaseModel):
reaction: Literal["like", "dislike"] | None = None


class SparkReactionResponse(BaseModel):
idea_id: str
user_reaction: Literal["like", "dislike"] | None = None
popularity_score: int


class SparkConvertRequest(BaseModel):
Expand Down Expand Up @@ -994,7 +1006,8 @@ async def get_spark_ideas(

rows = await db.execute(text(f"""
SELECT idea_id, title, description, rationale, signal_type, signal_label,
target_dimensions, target_nodes, complexity, estimated_impact, similar_agents
target_dimensions, target_nodes, complexity, estimated_impact, similar_agents,
user_reaction, popularity_score
FROM core.spark_ideas
WHERE {where}
ORDER BY updated_at DESC
Expand All @@ -1014,10 +1027,55 @@ async def get_spark_ideas(
complexity=r["complexity"] or "Medium",
estimated_impact=r["estimated_impact"] or "Medium",
similar_agents=[SparkSimilarAgent(**a) for a in (r["similar_agents"] or [])],
user_reaction=r["user_reaction"],
popularity_score=r["popularity_score"] or 0,
))
return result


@router.patch("/ideas/{idea_id}/reaction", response_model=SparkReactionResponse)
async def update_spark_idea_reaction(
idea_id: str,
payload: SparkReactionRequest,
company_id: str = Query(..., description="Company UUID"),
db: AsyncSession = Depends(get_db),
) -> SparkReactionResponse:
"""Persist the current user's Spark idea reaction and derived popularity score."""
current = await db.execute(text("""
SELECT user_reaction, popularity_score
FROM core.spark_ideas
WHERE company_id = :company_id AND idea_id = :idea_id
"""), {"company_id": company_id, "idea_id": idea_id})
row = current.mappings().first()
if row is None:
raise HTTPException(status_code=404, detail="Spark idea not found")

previous_reaction = row["user_reaction"]
previous_value = 1 if previous_reaction == "like" else -1 if previous_reaction == "dislike" else 0
next_value = 1 if payload.reaction == "like" else -1 if payload.reaction == "dislike" else 0
popularity_score = int(row["popularity_score"] or 0) - previous_value + next_value

await db.execute(text("""
UPDATE core.spark_ideas
SET user_reaction = :reaction,
popularity_score = :popularity_score,
updated_at = NOW()
WHERE company_id = :company_id AND idea_id = :idea_id
"""), {
"company_id": company_id,
"idea_id": idea_id,
"reaction": payload.reaction,
"popularity_score": popularity_score,
})
await db.commit()

return SparkReactionResponse(
idea_id=idea_id,
user_reaction=payload.reaction,
popularity_score=popularity_score,
)


@router.delete("/ideas", status_code=204)
async def reset_spark_ideas(
company_id: str = Query(..., description="Company UUID"),
Expand Down Expand Up @@ -1081,7 +1139,12 @@ async def generate_spark_ideas(
if not direction_clean:
async with AsyncSessionLocal() as clear_db:
await clear_db.execute(
text("DELETE FROM core.spark_ideas WHERE company_id = :company_id"),
text("""
DELETE FROM core.spark_ideas
WHERE company_id = :company_id
AND user_reaction IS NULL
AND COALESCE(popularity_score, 0) = 0
"""),
{"company_id": company_id},
)
await clear_db.commit()
Expand Down Expand Up @@ -1220,7 +1283,12 @@ async def event_stream() -> AsyncGenerator[str, None]:
if not direction_clean:
async with AsyncSessionLocal() as clear_db:
await clear_db.execute(
text("DELETE FROM core.spark_ideas WHERE company_id = :company_id"),
text("""
DELETE FROM core.spark_ideas
WHERE company_id = :company_id
AND user_reaction IS NULL
AND COALESCE(popularity_score, 0) = 0
"""),
{"company_id": company_id},
)
await clear_db.commit()
Expand Down
1 change: 1 addition & 0 deletions tavro_app/src/components/AgentHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ const AgentHeader: React.FC<AgentHeaderProps> = ({
)}
</div>
</div>
</div>
);
};

Expand Down
Loading