File size: 8,287 Bytes
f44aac9
 
 
 
 
 
 
 
 
 
9eec184
f44aac9
 
 
 
 
 
 
 
9eec184
96da637
 
 
 
 
 
9e8a876
96da637
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e8a876
96da637
 
 
 
9eec184
96da637
 
9eec184
 
 
96da637
9eec184
96da637
 
 
9eec184
 
96da637
f44aac9
9eec184
 
3b181a1
9eec184
3b181a1
 
9eec184
3b181a1
9eec184
 
 
 
 
 
3b181a1
 
9eec184
 
 
 
3b181a1
 
f44aac9
 
 
 
 
9eec184
f44aac9
7575503
f44aac9
 
 
 
 
 
9eec184
f44aac9
7575503
f44aac9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9eec184
f44aac9
52c63cf
9eec184
f44aac9
 
 
 
9eec184
f44aac9
 
 
 
 
9eec184
f44aac9
 
 
9392ef3
f44aac9
9e8a876
9392ef3
9e8a876
 
 
f44aac9
9392ef3
 
 
9eec184
9392ef3
 
9e8a876
9392ef3
f44aac9
 
 
 
 
 
 
 
 
 
 
 
dd8c015
 
 
 
 
 
 
 
f44aac9
dd8c015
f44aac9
 
52c63cf
 
 
 
9392ef3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f44aac9
 
 
 
 
 
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any
import uuid

from hackathon_advisor.data import Project, ProjectIndex, WhitespaceItem
from hackathon_advisor.scoring import ScoreCard, score_idea


GOALS = [
    "Off the Grid",
    "Well-Tuned",
    "Off-Brand",
    "Llama Champion",
    "Sharing is Caring",
    "Field Notes",
]

GOAL_PROFILE_BY_ID = {
    "Off the Grid": {
        "label": "Local-first",
        "description": "Favor ideas that work without proprietary inference APIs.",
    },
    "Well-Tuned": {
        "label": "Trainable",
        "description": "Shape good examples into a tiny fine-tune dataset.",
    },
    "Off-Brand": {
        "label": "Distinct voice",
        "description": "Leave room for an interface and tone people remember.",
    },
    "Llama Champion": {
        "label": "llama.cpp path",
        "description": "Prefer small-model choices that can run locally.",
    },
    "Sharing is Caring": {
        "label": "Shareable artifact",
        "description": "Make an output people can save, post, or compare.",
    },
    "Field Notes": {
        "label": "Build notes",
        "description": "Keep decisions easy to write up from the saved session.",
    },
}


def goal_profiles() -> list[dict[str, str]]:
    return [
        {
            "id": goal,
            "label": GOAL_PROFILE_BY_ID[goal]["label"],
            "description": GOAL_PROFILE_BY_ID[goal]["description"],
        }
        for goal in GOALS
    ]


def goal_label(goal: str) -> str:
    return GOAL_PROFILE_BY_ID.get(goal, {}).get("label", goal)


def normalize_goals(raw_goals: Any, default: list[str] | None = None) -> list[str]:
    if raw_goals is None:
        return list(default or [])
    if not isinstance(raw_goals, list):
        return list(default or [])

    goals: list[str] = []
    seen: set[str] = set()
    for raw_goal in raw_goals:
        goal = str(raw_goal)
        if goal in GOALS and goal not in seen:
            goals.append(goal)
            seen.add(goal)
    return goals


def goals_from_state(state: dict[str, Any]) -> list[str]:
    if "goals" not in state:
        return GOALS[:3]
    return normalize_goals(state.get("goals"), default=[])


@dataclass
class Idea:
    id: str
    title: str
    pitch: str
    goals: list[str] = field(default_factory=lambda: GOALS[:3])
    score: dict | None = None
    artifact: dict[str, Any] | None = None

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "title": self.title,
            "pitch": self.pitch,
            "goals": self.goals,
            "score": self.score,
            "artifact": self.artifact,
        }


@dataclass(frozen=True)
class ToolEvent:
    name: str
    summary: str

    def to_dict(self) -> dict:
        return {"name": self.name, "summary": self.summary}


class AdvisorTools:
    def __init__(self, index: ProjectIndex) -> None:
        self.index = index

    def list_projects(self, limit: int = 8) -> tuple[list[Project], ToolEvent]:
        projects = self.index.top_projects(limit=limit)
        return projects, ToolEvent("list_projects", f"Read {len(projects)} prominent Space cards.")

    def search_projects(self, query: str, limit: int = 5) -> tuple[list[Project], ToolEvent]:
        hits = self.index.search(query, limit=limit)
        projects = [hit.project for hit in hits]
        return projects, ToolEvent("search_projects", f"Found {len(projects)} nearby Space echoes.")

    def find_whitespace(self, limit: int = 5) -> tuple[list[WhitespaceItem], ToolEvent]:
        items = self.index.find_whitespace(limit=limit)
        return items, ToolEvent("find_whitespace", f"Ranked {len(items)} under-explored regions.")

    def save_idea(self, state: dict[str, Any], title: str, pitch: str) -> tuple[Idea, ToolEvent]:
        ideas = [Idea(**item) for item in state.get("ideas", [])]
        current_id = state.get("current_idea_id")
        goals = goals_from_state(state)
        idea = next((item for item in ideas if item.id == current_id), None)
        if idea is None or _is_new_idea(idea, title, pitch):
            idea = Idea(id=uuid.uuid4().hex[:8], title=title, pitch=pitch, goals=goals)
            ideas.append(idea)
        else:
            idea.title = title
            idea.pitch = pitch
            idea.goals = goals
        state["ideas"] = [item.to_dict() for item in ideas]
        state["current_idea_id"] = idea.id
        return idea, ToolEvent("save_idea", f"Wrote idea page '{idea.title}'.")

    def score_idea(self, idea: Idea) -> tuple[ScoreCard, ToolEvent]:
        score = score_idea(self.index, idea.title, idea.pitch, idea.goals)
        idea.score = score.to_dict()
        return score, ToolEvent("score_idea", f"Pressed a five-quadrant seal: {score.overall}/10.")

    def make_plan(self, idea: Idea, profile: dict[str, Any] | None = None) -> tuple[list[str], ToolEvent]:
        plan = [
            "Lock a one-sentence promise and one test input that proves what is different.",
            "Compare against the nearest echoes, then sharpen the part only this idea can own.",
            "Build the smallest happy path: input, nearby project citations, score, and one shareable output.",
            "Add one selected-goal feature only after the core loop is smooth enough to explain without narration.",
            "Write build notes from the exact decisions, screenshots, and outputs.",
        ]
        profile_steps = profile_plan_steps(profile)
        if profile_steps:
            plan[1:1] = profile_steps
        if any("Well" in goal for goal in idea.goals):
            plan.insert(
                max(0, len(plan) - 1),
                "Collect successful advisor examples before training a tiny LoRA.",
            )
        return plan, ToolEvent("make_plan", f"Drafted {len(plan)} build steps.")


def idea_from_text(text: str) -> tuple[str, str]:
    cleaned = " ".join(text.strip().split())
    if not cleaned:
        return "Blank Page", "A project direction waiting for one concrete user and one concrete tension."
    title = cleaned
    for prefix in ("i want to build", "build", "make", "my idea is", "idea:"):
        if cleaned.lower().startswith(prefix):
            title = cleaned[len(prefix) :].strip(" :-")
            break
    pitch = cleaned
    explicit_pitch = False
    if " -- " in title:
        title, pitch = (part.strip() for part in title.split(" -- ", 1))
        explicit_pitch = True
    raw_title = title
    title = raw_title[:64].strip(" .") or "Unwritten Page"
    if len(raw_title) > 64 or (not explicit_pitch and len(title) < len(cleaned)):
        title = f"{title[:58].strip()}..."
    return _display_title(title), pitch


def _is_new_idea(current: Idea, title: str, pitch: str) -> bool:
    return current.title.strip().casefold() != title.strip().casefold() or current.pitch.strip() != pitch.strip()


def profile_plan_steps(profile: dict[str, Any] | None) -> list[str]:
    if not isinstance(profile, dict):
        return []
    steps: list[str] = []
    time = _short_profile_value(profile.get("time"))
    skills = _short_profile_value(profile.get("skills"))
    constraints = _short_profile_value(profile.get("constraints"))
    preferences = _short_profile_value(profile.get("preferences"))
    if time:
        steps.append(f"Scope the first prototype to {time}; cut anything that cannot fit that window.")
    if skills:
        steps.append(f"Use your {skills} strength for the first working surface before adding new tooling.")
    if constraints:
        steps.append(f"Test the constraint early: {constraints}. Do this before polishing the artifact.")
    if preferences:
        steps.append(f"Shape the demo around {preferences} so the result feels intentional, not generic.")
    return steps


def _short_profile_value(value: Any, limit: int = 84) -> str:
    text = " ".join(str(value or "").split())
    if len(text) <= limit:
        return text
    return text[: limit - 3].rstrip(" ,.;:") + "..."


def _display_title(title: str) -> str:
    if not title:
        return "Unwritten Page"
    if any(char.isupper() or char.isdigit() for char in title):
        return title[0].upper() + title[1:]
    return title.capitalize()