Skip to content

Commit 195b4df

Browse files
committed
Add screenshot capture, OCR, Figma frame import, and prompt sharing
- Screenshot capture: pick a screen/window from the Attach menu via desktopCapturer and attach it as an image, reusing the existing vision attachment pipeline. - OCR: extract text from any attached image via tesseract.js (runs in the main process, offline after the first-use model download) and drop it into the composer. - Figma frame import: add a personal access token in Settings, then paste a "Copy link to selection" URL to fetch that frame as a PNG via the Figma REST API and attach it like any other image. - Prompt library export/import: save presets to a JSON file and import one from a teammate - simple file-based sharing since this app has no server component ("Team Prompt" from the earlier feature list). Also fixes typecheck-all.sh: its frontend step used `tsc --noEmit` on a tsconfig that only has "files": [] and project references - without -b/--build that's a no-op that silently checks nothing, which is how an invalid `Figma` icon import from lucide-react slipped past typecheck earlier and was only caught by the real `vite build`.
1 parent 93d3174 commit 195b4df

14 files changed

Lines changed: 750 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ Beyond chat, Modelforge includes an **agentic mode** — the model can read/writ
4040
**Files & retrieval**
4141
- Attach files, folders, images, video, and PDFs directly into a conversation.
4242
- Large folders are automatically chunked, embedded (via Ollama), and retrieved by relevance instead of dumped whole into the prompt — so a big project doesn't blow out a small model's context window.
43+
- **Screenshot capture** — pick a screen or window from the Attach menu and it's captured and attached as an image, no separate screenshot tool needed. (macOS may require granting Screen Recording permission the first time.)
44+
- **OCR** — extract plain text from any attached image with one click, dropped straight into the composer. Runs fully offline via [tesseract.js](https://github.com/naptha/tesseract.js) after its first use (which needs network access once, to download the ~2MB English text-recognition model).
45+
- **Figma frame import** — add a personal access token in Settings → Integrations, then paste a "Copy link to selection" URL from Figma to fetch that frame as an image, attached like any other screenshot.
46+
- **Prompt library sharing** — export your saved prompts to a JSON file and import one a teammate sent you (Settings → Prompt Library). There's no live sync — it's just a plain file, sent however you like.
4347

4448
**Agent mode** — see the [dedicated section](#agent-mode) below.
4549

‎app/package-lock.json‎

Lines changed: 123 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎app/package.json‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
"package.json"
3030
],
3131
"asarUnpack": [
32-
"**/node_modules/ffmpeg-static/**"
32+
"**/node_modules/ffmpeg-static/**",
33+
"**/node_modules/tesseract.js/**",
34+
"**/node_modules/tesseract.js-core/**"
3335
],
3436
"extraResources": [
3537
{
@@ -42,13 +44,27 @@
4244
},
4345
"linux": {
4446
"icon": "build/icon.png",
45-
"target": ["AppImage"]
47+
"target": [
48+
"AppImage"
49+
]
4650
},
4751
"mac": {
4852
"icon": "build/icon.png",
4953
"target": [
50-
{ "target": "dmg", "arch": ["x64", "arm64"] },
51-
{ "target": "zip", "arch": ["x64", "arm64"] }
54+
{
55+
"target": "dmg",
56+
"arch": [
57+
"x64",
58+
"arm64"
59+
]
60+
},
61+
{
62+
"target": "zip",
63+
"arch": [
64+
"x64",
65+
"arm64"
66+
]
67+
}
5268
]
5369
},
5470
"publish": {
@@ -71,6 +87,7 @@
7187
"dependencies": {
7288
"electron-updater": "^6.6.2",
7389
"ffmpeg-static": "^5.3.0",
74-
"pdf-parse": "^2.4.5"
90+
"pdf-parse": "^2.4.5",
91+
"tesseract.js": "^5.1.1"
7592
}
7693
}

‎app/src/data-transfer.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
33
import { app, dialog, shell, BrowserWindow } from "electron";
44
import * as sessionsStore from "./sessions-store";
55
import type { ChatSession } from "./sessions-store";
6+
import type { PromptPreset } from "./settings-store";
67

78
function sanitizeFilename(name: string): string {
89
return name.replace(/[/\\?%*:|"<>]/g, "-").slice(0, 80) || "chat";
@@ -75,6 +76,55 @@ export async function importSessions(win: BrowserWindow | null): Promise<{ impor
7576
return { imported };
7677
}
7778

79+
// Presets are shared between machines/teammates as a plain JSON file rather
80+
// than through any live sync — this app has no server component, so "share
81+
// with the team" means "send them this file".
82+
export async function exportPromptPresets(win: BrowserWindow | null, presets: PromptPreset[]): Promise<{ success: boolean }> {
83+
const date = new Date().toISOString().slice(0, 10);
84+
const options = {
85+
defaultPath: `modelforge-prompts-${date}.json`,
86+
filters: [{ name: "JSON", extensions: ["json"] }],
87+
};
88+
const result = win ? await dialog.showSaveDialog(win, options) : await dialog.showSaveDialog(options);
89+
if (result.canceled || !result.filePath) return { success: false };
90+
91+
fs.writeFileSync(result.filePath, JSON.stringify(presets, null, 2));
92+
return { success: true };
93+
}
94+
95+
function looksLikePreset(value: unknown): value is Partial<PromptPreset> {
96+
return !!value && typeof value === "object" && typeof (value as { prompt?: unknown }).prompt === "string";
97+
}
98+
99+
export async function importPromptPresets(win: BrowserWindow | null): Promise<PromptPreset[]> {
100+
const options = { properties: ["openFile" as const], filters: [{ name: "JSON", extensions: ["json"] }] };
101+
const result = win ? await dialog.showOpenDialog(win, options) : await dialog.showOpenDialog(options);
102+
if (result.canceled || result.filePaths.length === 0) return [];
103+
104+
let raw: unknown;
105+
try {
106+
raw = JSON.parse(fs.readFileSync(result.filePaths[0], "utf-8"));
107+
} catch {
108+
return [];
109+
}
110+
111+
const candidates: unknown[] = Array.isArray(raw) ? raw : [raw];
112+
const now = new Date().toISOString();
113+
const imported: PromptPreset[] = [];
114+
for (const candidate of candidates) {
115+
if (!looksLikePreset(candidate)) continue;
116+
imported.push({
117+
id: randomUUID(),
118+
name: typeof candidate.name === "string" && candidate.name ? candidate.name : "Imported prompt",
119+
prompt: candidate.prompt!,
120+
versions: [],
121+
createdAt: now,
122+
updatedAt: now,
123+
});
124+
}
125+
return imported;
126+
}
127+
78128
export function getUserDataPath(): string {
79129
return app.getPath("userData");
80130
}

‎app/src/figma.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, it, expect } from "vitest";
2+
import { parseFigmaUrl } from "./figma";
3+
4+
describe("parseFigmaUrl", () => {
5+
it("parses a classic /file/ link with a node-id", () => {
6+
expect(parseFigmaUrl("https://www.figma.com/file/abc123/My-Design?node-id=1-2")).toEqual({
7+
fileKey: "abc123",
8+
nodeId: "1:2",
9+
});
10+
});
11+
12+
it("parses a /design/ link with a node-id", () => {
13+
expect(parseFigmaUrl("https://www.figma.com/design/xyz789/My-Design?node-id=45-67&t=abc")).toEqual({
14+
fileKey: "xyz789",
15+
nodeId: "45:67",
16+
});
17+
});
18+
19+
it("decodes a URL-encoded node-id", () => {
20+
expect(parseFigmaUrl("https://www.figma.com/file/abc123/x?node-id=1%3A2")).toEqual({
21+
fileKey: "abc123",
22+
nodeId: "1:2",
23+
});
24+
});
25+
26+
it("returns null for a file link with no node-id (no specific frame selected)", () => {
27+
expect(parseFigmaUrl("https://www.figma.com/file/abc123/My-Design")).toBeNull();
28+
});
29+
30+
it("returns null for a non-Figma URL", () => {
31+
expect(parseFigmaUrl("https://example.com/not-figma")).toBeNull();
32+
});
33+
});

0 commit comments

Comments
 (0)