Skip to content
Closed
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
523 changes: 44 additions & 479 deletions Dayflow/Dayflow/Core/AI/GeminiDirectProvider.swift

Large diffs are not rendered by default.

159 changes: 147 additions & 12 deletions Dayflow/Dayflow/Core/AI/GeminiPromptPreferences.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

struct GeminiPromptOverrides: Codable, Equatable {
struct VideoPromptOverrides: Codable, Equatable {
var titleBlock: String?
var summaryBlock: String?
var detailedBlock: String?
Expand All @@ -14,21 +14,21 @@ struct GeminiPromptOverrides: Codable, Equatable {
}
}

enum GeminiPromptPreferences {
enum VideoPromptPreferences {
private static let overridesKey = "geminiPromptOverrides"
private static let store = UserDefaults.standard

static func load() -> GeminiPromptOverrides {
static func load() -> VideoPromptOverrides {
guard let data = store.data(forKey: overridesKey) else {
return GeminiPromptOverrides()
return VideoPromptOverrides()
}
guard let overrides = try? JSONDecoder().decode(GeminiPromptOverrides.self, from: data) else {
return GeminiPromptOverrides()
guard let overrides = try? JSONDecoder().decode(VideoPromptOverrides.self, from: data) else {
return VideoPromptOverrides()
}
return overrides
}

static func save(_ overrides: GeminiPromptOverrides) {
static func save(_ overrides: VideoPromptOverrides) {
guard let data = try? JSONEncoder().encode(overrides) else { return }
store.set(data, forKey: overridesKey)
}
Expand Down Expand Up @@ -145,17 +145,17 @@ enum GeminiPromptDefaults {
"""
}

struct GeminiPromptSections {
struct VideoPromptSections {
let title: String
let summary: String
let detailedSummary: String

init(overrides: GeminiPromptOverrides) {
self.title = GeminiPromptSections.compose(
init(overrides: VideoPromptOverrides) {
self.title = VideoPromptSections.compose(
defaultBlock: GeminiPromptDefaults.titleBlock, custom: overrides.titleBlock)
self.summary = GeminiPromptSections.compose(
self.summary = VideoPromptSections.compose(
defaultBlock: GeminiPromptDefaults.summaryBlock, custom: overrides.summaryBlock)
self.detailedSummary = GeminiPromptSections.compose(
self.detailedSummary = VideoPromptSections.compose(
defaultBlock: GeminiPromptDefaults.detailedSummaryBlock, custom: overrides.detailedBlock)
}

Expand All @@ -164,3 +164,138 @@ struct GeminiPromptSections {
return trimmed.isEmpty ? defaultBlock : trimmed
}
}

/// Shared prompt templates used by multiple LLM providers.
///
/// When prompts must remain *exactly* identical between providers, keep them here and call these helpers.
enum LLMPromptTemplates {
static func screenRecordingTranscriptionPrompt(durationString: String, schema: String) -> String {
"""
Screen Recording Transcription (Reconstruct Mode)
Watch this screen recording and create an activity log detailed enough that someone could reconstruct the session.
CRITICAL: This video is exactly \(durationString) long. ALL timestamps must be within 00:00 to \(durationString). No gaps.
Identifying the active app: On macOS, the app name is always shown in the top-left corner of the screen, right next to the Apple () menu. Check this FIRST to identify which app is being used. Do NOT guess — read the actual name from the menu bar. If you can't read it clearly, describe it generically (e.g., "code editor," "browser," "messaging app") rather than guessing a specific product name. Common code editors like Cursor, VS Code, Xcode, and Zed all look similar but have different names in the menu bar.
For each segment, ask yourself:
"What EXACTLY did they do? What SPECIFIC things can I see?"
Capture:
- Exact app/site names visible (check menu bar for app name)
- Exact file names, URLs, page titles
- Exact usernames, search queries, messages
- Exact numbers, stats, prices shown
Bad: "Checked email"
Good: "Gmail: Read email from boss@company.com 'RE: Budget approval' - replied 'Looks good'"
Bad: "Browsing Twitter"
Good: "Twitter/X: Scrolled feed - viewed posts by @pmarca about AI, @sama thread on GPT-5 (12 tweets)"
Bad: "Working on code"
Good: "Editing StorageManager.swift in [exact app name from menu bar] - fixed type error on line 47, changed String to String?"
Segments:
- 3-8 segments total
- You may use 1 segment only if the user appears idle for most of the recording
- Group by GOAL not app (IDE + Terminal + Browser for the same task = 1 segment)
- Do not create gaps; cover the full timeline

Return a JSON array that follows the schema: \(schema)
"""
}

static func activityCardsPrompt(
existingCardsString: String,
transcriptText: String,
categoriesSection: String,
promptSections: VideoPromptSections,
languageBlock: String,
schema: String,
) -> String {
"""
# Timeline Card Generation

You're writing someone's personal work journal. You'll get raw activity logs — screenshots, app switches, URLs — and your job is to turn them into timeline cards that help this person remember what they actually did.

The test: when they scan their timeline tomorrow morning, each card should make them go "oh right, that."

Write as if you ARE the person jotting down notes about their day. Not an analyst writing a report. Not a manager filing a status update.

---

## Card Structure

Each card covers one cohesive chunk of activity, roughly 15–60 minutes.

- Minimum 10 minutes per card. If something would be shorter, fold it into the neighboring card that makes the most sense.
- Maximum 60 minutes. If a card runs longer, split it where the focus naturally shifts.
- No gaps or overlaps between cards. If there's a real gap in the source data, preserve it. Otherwise, cards should meet cleanly.

**When to start a new card:**
1. What's the main thing happening right now?
2. Does the next chunk of activity continue that same thing? → Keep extending.
3. Is there a brief unrelated detour (<5 min)? → Log it as a distraction, keep the card going.
4. Has the focus genuinely shifted for 10+ minutes? → New card.

---

\(promptSections.title)

---

\(promptSections.summary)

---

\(promptSections.detailedSummary)

\(languageBlock)

---

## Category

\(categoriesSection)

---

## Distractions

A distraction is a brief (<5 min) unrelated interruption inside a card. Checking X for 2 minutes while debugging is a distraction. Spending 15 minutes on X is not a distraction — it's either part of the card's theme or it's a new card.

Don't label related sub-tasks as distractions. Googling an error message while debugging isn't a distraction, it's part of debugging.

---

## App Sites

Identify the main app or website for each card.

- primary: the main app used in the card (canonical domain, lowercase, no protocol).
- secondary: another meaningful app used, or the enclosing app (e.g., browser). Omit if there isn't a clear one.

Be specific: docs.google.com not google.com, mail.google.com not google.com.

Common mappings:
- Figma → figma.com
- Notion → notion.so
- Google Docs → docs.google.com
- Gmail → mail.google.com
- VS Code → code.visualstudio.com
- Xcode → developer.apple.com/xcode
- Twitter/X → x.com
- Zoom → zoom.us
- ChatGPT → chatgpt.com

---

## Continuity Rules

Your output cards must cover the same total time range as the previous cards plus any new observations. Think of previous cards as a draft you're revising and extending, not locked history.

- Don't drop time segments that were previously covered.
- If new observations extend beyond the previous range, add cards to cover the new time.
- Preserve genuine gaps in the source data.

INPUTS:
Previous cards: \(existingCardsString)
New observations: \(transcriptText)

Return a JSON array of activity cards that follows the schema: \(schema)
"""
}
}
106 changes: 106 additions & 0 deletions Dayflow/Dayflow/Core/AI/LLMSchema.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Foundation

enum LLMSchema {
static let screenRecordingTranscriptionSchema: String = """
{
"type": "array",
"items": {
"type": "object",
"properties": {
"startTimestamp": {
"type": "string",
"description": "The start timestamp of the segment in 'MM:SS' format."
},
"endTimestamp": {
"type": "string",
"description": "The end timestamp of the segment in 'MM:SS' format."
},
"description": {
"type": "string",
"description": "A 1-3 sentence description of the activity in the segment."
}
},
"required": ["startTimestamp", "endTimestamp", "description"]
}
}
"""

static let activityCardsSchema: String = """
{
"type": "array",
"items": {
"type": "object",
"properties": {
"startTime": {
"type": "string",
"description": "The start time of the activity card in 'h:mm a' format (e.g., '1:12 AM')."
},
"endTime": {
"type": "string",
"description": "The end time of the activity card in 'h:mm a' format (e.g., '1:30 AM')."
},
"category": {
"type": "string",
"description": "The category of the activity."
},
"subcategory": {
"type": "string",
"description": "The subcategory of the activity."
},
"title": {
"type": "string",
"description": "A concise title for the activity card."
},
"summary": {
"type": "string",
"description": "A 2-3 sentence summary of the activity."
},
"detailedSummary": {
"type": "string",
"description": "A detailed, granular log of the activity."
},
"distractions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"startTime": {
"type": "string",
"description": "The start time of the distraction in 'h:mm a' format."
},
"endTime": {
"type": "string",
"description": "The end time of the distraction in 'h:mm a' format."
},
"title": {
"type": "string",
"description": "A title for the distraction."
},
"summary": {
"type": "string",
"description": "A summary of the distraction."
}
},
"required": ["startTime", "endTime", "title", "summary"]
}
},
"appSites": {
"type": "object",
"properties": {
"primary": {
"type": "string",
"description": "The primary app or website used."
},
"secondary": {
"type": "string",
"description": "The secondary app or website used."
}
},
"required": ["primary"]
}
},
"required": ["startTime", "endTime", "category", "title", "summary", "detailedSummary", "appSites"]
}
}
"""
}
Loading
Loading