dopple-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/integrations/figma.ts
4
+ async function reviewFigmaDesign(llm, personas, config) {
5
+ const fileData = await fetchFigmaFile(config);
6
+ const designDescription = buildDesignDescription(fileData, config.nodeId);
7
+ const imageUrl = await fetchFigmaImage(config);
8
+ const reviews = [];
9
+ for (const persona of personas) {
10
+ const review = await llm.generateJSON(
11
+ persona.toFullPrompt("design review") + "\n\nYou are reviewing a product design. React as yourself \u2014 drawing on your full context: your personality, how you use this product, what you need from it, your frustrations. This isn't generic feedback \u2014 it's YOUR reaction.",
12
+ `Review this design:
13
+
14
+ Design structure:
15
+ ${designDescription}
16
+
17
+ ${imageUrl ? `Design image: ${imageUrl}` : ""}
18
+
19
+ As this persona, provide your honest review. Return JSON:
20
+ {
21
+ "firstImpression": "<1-2 sentences: your gut reaction seeing this design>",
22
+ "messagingFeedback": "<does the text/copy speak to you? Is it clear? Does it resonate?>",
23
+ "usabilityFeedback": "<can you figure out what to do? Is the layout intuitive?>",
24
+ "emotionalResponse": "<how does this design make you feel? Excited? Confused? Trusted? Skeptical?>",
25
+ "suggestions": ["<specific thing to change>", "<another>"],
26
+ "trustSignal": "<high|medium|low \u2014 would you trust this product based on the design?>",
27
+ "citations": [{"source": "trait-model", "detail": "<which trait drives this feedback>", "weight": <0-1>}]
28
+ }`
29
+ );
30
+ reviews.push({
31
+ personaId: persona.id,
32
+ personaName: persona.name,
33
+ ...review
34
+ });
35
+ }
36
+ const consensus = await llm.generateJSON(
37
+ "You synthesize design feedback from multiple reviewers into actionable consensus.",
38
+ `${reviews.length} personas reviewed a design. Their feedback:
39
+
40
+ ${reviews.map((r) => `${r.personaName}:
41
+ Impression: ${r.firstImpression}
42
+ Messaging: ${r.messagingFeedback}
43
+ Usability: ${r.usabilityFeedback}
44
+ Emotion: ${r.emotionalResponse}
45
+ Trust: ${r.trustSignal}
46
+ Suggestions: ${r.suggestions.join("; ")}`).join("\n\n")}
47
+
48
+ Synthesize into JSON:
49
+ {
50
+ "strengths": ["<things most personas liked>"],
51
+ "weaknesses": ["<things most personas struggled with>"],
52
+ "suggestions": ["<top 3-5 actionable changes, ranked by how many personas mentioned them>"]
53
+ }`
54
+ );
55
+ return {
56
+ fileKey: config.fileKey,
57
+ fileName: fileData.name ?? config.fileKey,
58
+ nodeId: config.nodeId,
59
+ reviews,
60
+ consensus,
61
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
62
+ };
63
+ }
64
+ async function fetchFigmaFile(config) {
65
+ const url = config.nodeId ? `https://api.figma.com/v1/files/${config.fileKey}/nodes?ids=${config.nodeId}` : `https://api.figma.com/v1/files/${config.fileKey}?depth=3`;
66
+ const res = await fetch(url, {
67
+ headers: { "X-Figma-Token": config.accessToken }
68
+ });
69
+ if (!res.ok) {
70
+ throw new Error(`Figma API error: ${res.status} ${res.statusText}`);
71
+ }
72
+ return await res.json();
73
+ }
74
+ async function fetchFigmaImage(config) {
75
+ if (!config.nodeId) return null;
76
+ const res = await fetch(
77
+ `https://api.figma.com/v1/images/${config.fileKey}?ids=${config.nodeId}&format=png&scale=2`,
78
+ {
79
+ headers: { "X-Figma-Token": config.accessToken }
80
+ }
81
+ );
82
+ if (!res.ok) return null;
83
+ const data = await res.json();
84
+ return Object.values(data.images)[0] ?? null;
85
+ }
86
+ function buildDesignDescription(fileData, nodeId) {
87
+ const lines = [`File: ${fileData.name}`];
88
+ extractTextFromNode(fileData.document, lines, 0);
89
+ return lines.join("\n");
90
+ }
91
+ function extractTextFromNode(node, lines, depth) {
92
+ const indent = " ".repeat(depth);
93
+ if (node.type === "TEXT" && node.characters) {
94
+ lines.push(`${indent}[Text] "${node.characters}"`);
95
+ } else if (node.name) {
96
+ lines.push(`${indent}[${node.type}] ${node.name}`);
97
+ }
98
+ if (node.children) {
99
+ for (const child of node.children) {
100
+ extractTextFromNode(child, lines, depth + 1);
101
+ }
102
+ }
103
+ }
104
+ export {
105
+ reviewFigmaDesign
106
+ };
107
+ //# sourceMappingURL=figma-N554M5KW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/integrations/figma.ts"],"sourcesContent":["/**\n * Figma integration.\n *\n * Fetches a Figma design file and has personas review it.\n * Personas evaluate: layout, messaging, clarity, emotional response, usability.\n *\n * @example\n * ```ts\n * const review = await reviewFigmaDesign(llm, personas, {\n * fileKey: \"abc123\",\n * accessToken: \"fig_...\",\n * nodeId: \"42:0\", // optional — specific frame\n * });\n * ```\n */\n\nimport type { LLMProvider, SourceCitation } from \"../types.js\";\nimport { Persona } from \"../persona/persona.js\";\n\nexport interface FigmaConfig {\n /** Figma personal access token */\n accessToken: string;\n /** Figma file key (from the URL) */\n fileKey: string;\n /** Specific node/frame ID to review (optional — reviews whole file if omitted) */\n nodeId?: string;\n}\n\nexport interface DesignReviewResponse {\n personaId: string;\n personaName: string;\n /** Overall impression (1-2 sentences) */\n firstImpression: string;\n /** Does the messaging resonate with this persona? */\n messagingFeedback: string;\n /** Is the layout clear and usable? */\n usabilityFeedback: string;\n /** Emotional response — how does the design make them feel? */\n emotionalResponse: string;\n /** What would they change? */\n suggestions: string[];\n /** Would they trust/use this product based on the design? */\n trustSignal: \"high\" | \"medium\" | \"low\";\n citations: SourceCitation[];\n}\n\nexport interface DesignReview {\n fileKey: string;\n fileName: string;\n nodeId?: string;\n reviews: DesignReviewResponse[];\n /** Aggregated: what most personas agree on */\n consensus: {\n strengths: string[];\n weaknesses: string[];\n suggestions: string[];\n };\n timestamp: string;\n}\n\n/**\n * Fetch a Figma design and have personas review it.\n */\nexport async function reviewFigmaDesign(\n llm: LLMProvider,\n personas: Persona[],\n config: FigmaConfig\n): Promise<DesignReview> {\n // Fetch file metadata\n const fileData = await fetchFigmaFile(config);\n\n // Build a text description of the design for personas\n const designDescription = buildDesignDescription(fileData, config.nodeId);\n\n // Get image URL for the frame\n const imageUrl = await fetchFigmaImage(config);\n\n // Have each persona review\n const reviews: DesignReviewResponse[] = [];\n\n for (const persona of personas) {\n const review = await llm.generateJSON<{\n firstImpression: string;\n messagingFeedback: string;\n usabilityFeedback: string;\n emotionalResponse: string;\n suggestions: string[];\n trustSignal: \"high\" | \"medium\" | \"low\";\n citations: SourceCitation[];\n }>(\n persona.toFullPrompt(\"design review\") +\n \"\\n\\nYou are reviewing a product design. React as yourself — drawing on your full context: your personality, how you use this product, what you need from it, your frustrations. This isn't generic feedback — it's YOUR reaction.\",\n `Review this design:\n\nDesign structure:\n${designDescription}\n\n${imageUrl ? `Design image: ${imageUrl}` : \"\"}\n\nAs this persona, provide your honest review. Return JSON:\n{\n \"firstImpression\": \"<1-2 sentences: your gut reaction seeing this design>\",\n \"messagingFeedback\": \"<does the text/copy speak to you? Is it clear? Does it resonate?>\",\n \"usabilityFeedback\": \"<can you figure out what to do? Is the layout intuitive?>\",\n \"emotionalResponse\": \"<how does this design make you feel? Excited? Confused? Trusted? Skeptical?>\",\n \"suggestions\": [\"<specific thing to change>\", \"<another>\"],\n \"trustSignal\": \"<high|medium|low — would you trust this product based on the design?>\",\n \"citations\": [{\"source\": \"trait-model\", \"detail\": \"<which trait drives this feedback>\", \"weight\": <0-1>}]\n}`\n );\n\n reviews.push({\n personaId: persona.id,\n personaName: persona.name,\n ...review,\n });\n }\n\n // Aggregate consensus\n const consensus = await llm.generateJSON<{\n strengths: string[];\n weaknesses: string[];\n suggestions: string[];\n }>(\n \"You synthesize design feedback from multiple reviewers into actionable consensus.\",\n `${reviews.length} personas reviewed a design. Their feedback:\n\n${reviews.map((r) => `${r.personaName}:\n Impression: ${r.firstImpression}\n Messaging: ${r.messagingFeedback}\n Usability: ${r.usabilityFeedback}\n Emotion: ${r.emotionalResponse}\n Trust: ${r.trustSignal}\n Suggestions: ${r.suggestions.join(\"; \")}`).join(\"\\n\\n\")}\n\nSynthesize into JSON:\n{\n \"strengths\": [\"<things most personas liked>\"],\n \"weaknesses\": [\"<things most personas struggled with>\"],\n \"suggestions\": [\"<top 3-5 actionable changes, ranked by how many personas mentioned them>\"]\n}`\n );\n\n return {\n fileKey: config.fileKey,\n fileName: fileData.name ?? config.fileKey,\n nodeId: config.nodeId,\n reviews,\n consensus,\n timestamp: new Date().toISOString(),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Figma API helpers\n// ---------------------------------------------------------------------------\n\nasync function fetchFigmaFile(\n config: FigmaConfig\n): Promise<{ name: string; document: FigmaNode }> {\n const url = config.nodeId\n ? `https://api.figma.com/v1/files/${config.fileKey}/nodes?ids=${config.nodeId}`\n : `https://api.figma.com/v1/files/${config.fileKey}?depth=3`;\n\n const res = await fetch(url, {\n headers: { \"X-Figma-Token\": config.accessToken },\n });\n\n if (!res.ok) {\n throw new Error(`Figma API error: ${res.status} ${res.statusText}`);\n }\n\n return (await res.json()) as { name: string; document: FigmaNode };\n}\n\nasync function fetchFigmaImage(\n config: FigmaConfig\n): Promise<string | null> {\n if (!config.nodeId) return null;\n\n const res = await fetch(\n `https://api.figma.com/v1/images/${config.fileKey}?ids=${config.nodeId}&format=png&scale=2`,\n {\n headers: { \"X-Figma-Token\": config.accessToken },\n }\n );\n\n if (!res.ok) return null;\n\n const data = (await res.json()) as {\n images: Record<string, string>;\n };\n return Object.values(data.images)[0] ?? null;\n}\n\nfunction buildDesignDescription(\n fileData: { name: string; document: FigmaNode },\n nodeId?: string\n): string {\n const lines: string[] = [`File: ${fileData.name}`];\n extractTextFromNode(fileData.document, lines, 0);\n return lines.join(\"\\n\");\n}\n\nfunction extractTextFromNode(\n node: FigmaNode,\n lines: string[],\n depth: number\n): void {\n const indent = \" \".repeat(depth);\n\n if (node.type === \"TEXT\" && node.characters) {\n lines.push(`${indent}[Text] \"${node.characters}\"`);\n } else if (node.name) {\n lines.push(`${indent}[${node.type}] ${node.name}`);\n }\n\n if (node.children) {\n for (const child of node.children) {\n extractTextFromNode(child, lines, depth + 1);\n }\n }\n}\n\ninterface FigmaNode {\n type: string;\n name?: string;\n characters?: string;\n children?: FigmaNode[];\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DoppleGraph
4
+ } from "./chunk-FA7ZWJOA.js";
5
+ import "./chunk-PGZVVIL6.js";
6
+ export {
7
+ DoppleGraph
8
+ };
9
+ //# sourceMappingURL=graph-P5GYGDF7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}