chore: remove twenty-companion dead code (#23310)

## What

Removes `packages/twenty-companion` (package name `twenty-desktop`), the
Electron "Twenty Desktop" proof of concept that landed with the
Recall.ai call-recording work in #18281.

## Why it's dead code

- **Not in the nx graph** — no `project.json`, so no target ever runs
against it.
- **Not in CI** — no workflow references it. #21327 said as much when
bumping its Electron: "there's no CI job that builds/tests
twenty-companion, so this isn't exercised by CI".
- **No code references** — nothing imports it, and the only path
references were the root `workspaces` array, `yarn.lock`, and
`.vscode/twenty.code-workspace`. It talks to Twenty over the public REST
API from a separate process, so there is no coupling to remove.
- **Self-declared POC** — its README opens with "This application is a
Proof of Concept (POC) and must NOT be used in production. [...]
Security, stability, and performance have not been validated for
production use."
- **No feature work since it landed** (March 2026). Every commit
touching it since has been a dependency or tooling sweep: React 19
migration, ESLint→OxLint, npm→yarn workspaces, and four CVE bumps.
- **Docs already stale** — its README points at
`packages/twenty-apps/internal/call-recording`, which no longer exists.
The shipped app lives at `packages/twenty-apps/public/call-recorder` and
does not reference the desktop companion.

Meanwhile it pulled a full Electron + electron-forge toolchain into
every root install, and kept generating Dependabot noise against a tree
nothing builds.

## Changes

- Delete `packages/twenty-companion`.
- Drop its entry from root `workspaces` and from
`.vscode/twenty.code-workspace`.
- Drop four root `resolutions` that existed only to evict CVEs from the
Electron tree, along with their entries in the `//resolutions` rationale
doc:
  - `@electron/rebuild/tar`, `@electron/node-gyp/tar`
  - `@electron-forge/plugin-webpack/webpack-dev-server`
- `make-fetch-happen` — its only sub-`^15` consumer was the Electron
`node-gyp` fork; the remaining consumers (`@sigstore/sign`,
`npm-registry-fetch`, `tuf-js`) already declare `^15.x`
- Regenerate `yarn.lock`.

## Lockfile impact

469 descriptors removed, **zero version changes for any surviving
descriptor** (verified with a descriptor-level diff of old vs new
resolutions). Two descriptors show up as new —
`make-fetch-happen@npm:^15.0.1` and `@npm:^15.0.4` — only because the
global resolution was previously rewriting them; both still resolve to
`15.0.6`. Re-running resolution produces a byte-identical lockfile.

## Test plan

- [x] Repo-wide grep confirms no remaining references to
`twenty-companion` / `twenty-desktop` / the Electron toolchain.
- [x] `yarn install --mode=update-lockfile` is stable and idempotent
under hardened mode.
- [x] Descriptor-level lockfile diff shows no resolution changes outside
the removed tree.
- [ ] CI green (nothing targets the removed package, so the risk surface
is the lockfile).


---
_Generated by [Claude
Code](https://claude.ai/code/session_019NttPZiJWSJz56RW8pZ5jN)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23310?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-07-26 18:52:52 +02:00
committed by GitHub
parent 0b44864f5f
commit 24067ec87a
32 changed files with 170 additions and 10802 deletions
-4
View File
@@ -68,10 +68,6 @@
"name": "packages/twenty-client-sdk",
"path": "../packages/twenty-client-sdk"
},
{
"name": "packages/twenty-companion",
"path": "../packages/twenty-companion"
},
{
"name": "packages/twenty-front-component-renderer",
"path": "../packages/twenty-front-component-renderer"
+1 -6
View File
File diff suppressed because one or more lines are too long
-9
View File
@@ -1,9 +0,0 @@
RECALLAI_API_URL=https://us-west-2.recall.ai
RECALLAI_API_KEY=recall_api_key
# Twenty CRM integration (optional)
# Generate an API key at <your-twenty-instance>/settings/api-webhooks
TWENTY_API_URL=http://localhost:3000
# Workspace subdomain, required for logic function routes when multiworkspace is enabled
TWENTY_WORKSPACE_SUBDOMAIN=
TWENTY_API_KEY=replace_me_with_your_twenty_api_key
-2
View File
@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
-92
View File
@@ -1,92 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional oxlint cache
.oxlintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Vite
.vite/
# Electron-Forge
out/
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.print</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>
-7
View File
@@ -1,7 +0,0 @@
Copyright 2025 Recall.ai
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-52
View File
@@ -1,52 +0,0 @@
# Twenty Desktop
> **WARNING: This application is a Proof of Concept (POC) and must NOT be used in production.** It is intended for demonstration and experimentation purposes only. Security, stability, and performance have not been validated for production use.
This is a demo application that shows off what you can build with the [Recall.ai Desktop Recording SDK.](https://www.recall.ai/product/desktop-recording-sdk)
This repo is intended to be a mockup of the kind of experience you can build using the Desktop Recording SDK.
Need help? Reach out to our support team [support@recall.ai](mailto:support@recall.ai).
# Setup
- Copy the `env.example` file to a `.env` file:
- `cp .env.example .env`
- Replace `RECALLAI_API_URL` with the base URL for the [Recall region](https://docs.recall.ai/docs/regions#/) that you're using that matches your API key, example:
- `RECALLAI_API_URL=https://us-east-1.recall.ai`
- Modify `.env` to include your Recall.ai API key:
- `RECALLAI_API_KEY=<your key>`
Required: This project also uses live transcription with Assembly AI. You'll need to configure your own Assembly credentials on the Recall.ai dashboard. Follow our [AssemblyAI real-time transcription guide](https://docs.recall.ai/docs/dsdk-realtime-transcription#assemblyai-transcription-setup) to set this up.
If you want to enable the AI summary after a recording is finished, you can specify an OpenRouter API key.
```
OPENROUTER_KEY=<your key>
```
### Twenty CRM Integration (optional)
To automatically create `callRecording` records in Twenty when a meeting starts (and mark them as ended when the meeting closes), configure:
```
TWENTY_API_URL=http://localhost:3000
TWENTY_API_KEY=<your key>
```
The `call-recording` Twenty app must be installed in your workspace first (`packages/twenty-apps/internal/call-recording`). Generate an API key at `<your-twenty-instance>/settings/api-webhooks`.
To launch the Twenty Desktop application, start the server first, then the app:
```sh
npm ci
npm start
```
# Screenshots
![Screenshot 2025-06-16 at 10 10 57PM](https://github.com/user-attachments/assets/9df12246-b5be-466d-958e-e09ff0b4b3cb)
![Screenshot 2025-06-16 at 10 22 44PM](https://github.com/user-attachments/assets/685f13ab-7c02-4f29-a987-830d331c4d36)
![Screenshot 2025-06-16 at 10 14 38PM](https://github.com/user-attachments/assets/75817823-084c-46b0-bbe8-e0195a3f9051)
-89
View File
@@ -1,89 +0,0 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
module.exports = {
packagerConfig: {
asar: {
unpackDir: "node_modules/@recallai"
},
osxSign: process.env.SKIP_SIGN ? false : {
continueOnError: false,
optionsForFile: (_) => {
return {
entitlements: './Entitlements.plist'
};
}
},
icon: './twenty',
extendInfo: {
NSUserNotificationAlertStyle: "alert",
}
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-dmg'
},
// {
// name: '@electron-forge/maker-squirrel',
// config: {},
// },
// {
// name: '@electron-forge/maker-zip',
// platforms: ['darwin'],
// },
// {
// name: '@electron-forge/maker-deb',
// config: {},
// },
// {
// name: '@electron-forge/maker-rpm',
// config: {},
// },
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
{
name: '@electron-forge/plugin-webpack',
config: {
port: 3042,
devContentSecurityPolicy: "default-src * 'unsafe-inline' 'unsafe-eval' data: blob: filesystem: mediastream: file:;",
mainConfig: './webpack.main.config.js',
renderer: {
config: './webpack.renderer.config.js',
entryPoints: [
{
html: './src/index.html',
js: './src/renderer.js',
name: 'main_window',
preload: {
js: './src/preload.js',
},
},
],
},
},
},
{
name: "@timfish/forge-externals-plugin",
config: {
externals: ["@recallai/desktop-sdk"],
includeDeps: true
}
},
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};
-61
View File
@@ -1,61 +0,0 @@
{
"name": "twenty-desktop",
"productName": "Twenty",
"version": "1.0.0",
"description": "Twenty meeting recorder",
"main": ".webpack/main",
"scripts": {
"start": "concurrently \"yarn start:server\" \"yarn start:electron\"",
"start:electron": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "echo \"No linting configured\"",
"start:server": "node ./src/server.js"
},
"keywords": [],
"author": {
"name": "Nick Faro",
"email": "yux50000@hotmail.com"
},
"license": "MIT",
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@electron-forge/cli": "^7.8.0",
"@electron-forge/maker-deb": "^7.8.0",
"@electron-forge/maker-dmg": "^7.8.1",
"@electron-forge/maker-rpm": "^7.8.0",
"@electron-forge/maker-squirrel": "^7.8.0",
"@electron-forge/maker-zip": "^7.8.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.8.0",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-webpack": "^7.8.0",
"@electron/fuses": "^1.8.0",
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
"css-loader": "^6.11.0",
"electron": "^39.8.5",
"node-loader": "^2.1.0",
"style-loader": "^3.3.4"
},
"resolutions": {
"@electron/packager/@electron/osx-sign": "github:recallai/osx-sign"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.40.1",
"@recallai/desktop-sdk": "^2.0.0",
"@timfish/forge-externals-plugin": "^0.2.1",
"axios": "^1.16.0",
"codemirror": "^6.0.1",
"concurrently": "^10.0.0",
"dotenv": "^16.5.0",
"easymde": "^2.20.0",
"electron-squirrel-startup": "^1.0.1",
"express": "^4.18.2",
"marked": "^5.0.0",
"openai": "^4.97.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"zod": "^3.25.76"
}
}
-18
View File
@@ -1,18 +0,0 @@
#!/bin/sh
set -e
echo "==> Packaging Twenty Desktop..."
SKIP_SIGN=1 npx electron-forge package
echo "==> Ad-hoc signing Twenty.app..."
codesign --force --deep --sign - out/Twenty-darwin-arm64/Twenty.app
echo "==> Starting local server..."
node src/server.js &
SERVER_PID=$!
trap "echo '==> Stopping server...'; kill $SERVER_PID 2>/dev/null" EXIT INT TERM
echo "==> Launching Twenty.app..."
./out/Twenty-darwin-arm64/Twenty.app/Contents/MacOS/Twenty
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
npm start
@@ -1,5 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="20" fill="#f0f0f0"/>
<path d="M20 20C22.7614 20 25 17.7614 25 15C25 12.2386 22.7614 10 20 10C17.2386 10 15 12.2386 15 15C15 17.7614 17.2386 20 20 20Z" fill="#a0a0a0"/>
<path d="M12 31C12 26.0294 15.5817 22 20 22C24.4183 22 28 26.0294 28 31" stroke="#a0a0a0" stroke-width="4"/>
</svg>

Before

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

@@ -1,5 +0,0 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="96" height="96" rx="11.293" fill="#000000"/>
<path d="M19.25 35.746C19.25 30.5 23.508 26.246 28.75 26.246H47.039C47.309 26.246 47.555 26.406 47.668 26.652C47.781 26.902 47.73 27.191 47.547 27.395L43.539 31.75C42.84 32.508 41.859 32.945 40.828 32.945H28.801C27.227 32.945 25.949 34.223 25.949 35.797V42.98C25.949 43.906 25.199 44.652 24.273 44.652H20.93C20.004 44.652 19.258 43.906 19.258 42.98V35.746Z" fill="white"/>
<path d="M76.152 60.254C76.152 65.5 71.895 69.754 66.648 69.754H58.879C53.633 69.754 49.375 65.5 49.375 60.254V46.652C49.375 45.727 49.723 44.836 50.352 44.152L54.883 39.234C55.074 39.027 55.371 38.957 55.637 39.055C55.898 39.164 56.074 39.41 56.074 39.691V60.211C56.074 61.785 57.352 63.063 58.926 63.063H66.605C68.18 63.063 69.457 61.785 69.457 60.211V35.797C69.457 34.223 68.18 32.945 66.605 32.945H57.676C56.652 32.945 55.68 33.375 54.98 34.121L28.348 63.063H44.352C45.273 63.063 46.023 63.813 46.023 64.738V68.082C46.023 69.008 45.273 69.754 44.352 69.754H22.785C20.832 69.754 19.242 68.168 19.242 66.211V64.441C19.242 63.551 19.574 62.695 20.18 62.039L50.039 29.605C52.016 27.457 54.789 26.246 57.707 26.246H66.641C71.887 26.246 76.145 30.5 76.145 35.746V60.254Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because it is too large Load Diff
-286
View File
@@ -1,286 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Twenty - Meeting Notes</title>
</head>
<body>
<div class="app-container">
<header class="header" id="drag-region">
<div class="header-left">
<button class="btn back-btn" id="backButton" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 11H7.83L13.42 5.41L12 4L4 12L12 20L13.41 18.59L7.83 13H20V11Z" fill="currentColor"/>
</svg>
</button>
<div class="app-logo">
<svg width="28" height="28" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="96" height="96" rx="11.293" fill="#000000"/>
<path d="M19.25 35.746C19.25 30.5 23.508 26.246 28.75 26.246H47.039C47.309 26.246 47.555 26.406 47.668 26.652C47.781 26.902 47.73 27.191 47.547 27.395L43.539 31.75C42.84 32.508 41.859 32.945 40.828 32.945H28.801C27.227 32.945 25.949 34.223 25.949 35.797V42.98C25.949 43.906 25.199 44.652 24.273 44.652H20.93C20.004 44.652 19.258 43.906 19.258 42.98V35.746Z" fill="white"/>
<path d="M76.152 60.254C76.152 65.5 71.895 69.754 66.648 69.754H58.879C53.633 69.754 49.375 65.5 49.375 60.254V46.652C49.375 45.727 49.723 44.836 50.352 44.152L54.883 39.234C55.074 39.027 55.371 38.957 55.637 39.055C55.898 39.164 56.074 39.41 56.074 39.691V60.211C56.074 61.785 57.352 63.063 58.926 63.063H66.605C68.18 63.063 69.457 61.785 69.457 60.211V35.797C69.457 34.223 68.18 32.945 66.605 32.945H57.676C56.652 32.945 55.68 33.375 54.98 34.121L28.348 63.063H44.352C45.273 63.063 46.023 63.813 46.023 64.738V68.082C46.023 69.008 45.273 69.754 44.352 69.754H22.785C20.832 69.754 19.242 68.168 19.242 66.211V64.441C19.242 63.551 19.574 62.695 20.18 62.039L50.039 29.605C52.016 27.457 54.789 26.246 57.707 26.246H66.641C71.887 26.246 76.145 30.5 76.145 35.746V60.254Z" fill="white"/>
</svg>
</div>
</div>
<div class="header-center">
<div class="search-container">
<span class="search-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5 14H14.71L14.43 13.73C15.41 12.59 16 11.11 16 9.5C16 5.91 13.09 3 9.5 3C5.91 3 3 5.91 3 9.5C3 13.09 5.91 16 9.5 16C11.11 16 12.59 15.41 13.73 14.43L14 14.71V15.5L19 20.49L20.49 19L15.5 14ZM9.5 14C7.01 14 5 11.99 5 9.5C5 7.01 7.01 5 9.5 5C11.99 5 14 7.01 14 9.5C14 11.99 11.99 14 9.5 14Z" fill="#666666"/>
</svg>
</span>
<input type="text" class="search-input" placeholder="Search notes">
</div>
</div>
<div class="header-right">
<button class="btn new-note-btn" id="newNoteBtn">Record In-person Meeting</button>
<button class="btn join-meeting-btn" id="joinMeetingBtn">Record Meeting</button>
<button id="toggleSidebar" class="btn toggle-sidebar-btn" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 18H21V16H3V18ZM3 13H21V11H3V13ZM3 6V8H21V6H3Z" fill="#666666"/>
</svg>
</button>
<div class="user-avatar">
<svg width="32" height="32" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="20" fill="#f0f0f0"/>
<path d="M20 20C22.7614 20 25 17.7614 25 15C25 12.2386 22.7614 10 20 10C17.2386 10 15 12.2386 15 15C15 17.7614 17.2386 20 20 20Z" fill="#a0a0a0"/>
<path d="M12 31C12 26.0294 15.5817 22 20 22C24.4183 22 28 26.0294 28 31" stroke="#a0a0a0" stroke-width="4"/>
</svg>
</div>
</div>
</header>
<!-- Home Screen View -->
<div id="homeView">
<main class="main-content">
<div class="content-container">
<!-- Only showing notes section, calendar hidden -->
<section class="meetings-section">
<h2 class="section-title">Notes</h2>
<div class="meetings-list" id="notes-list">
<!-- Meeting cards will be inserted here by JavaScript -->
</div>
</section>
</div>
</main>
</div>
<!-- Note Editor View -->
<div id="editorView" style="display: none;">
<div class="note-container">
<main class="editor-content">
<div class="note-header">
<div class="title-container">
<h1 id="noteTitle" contenteditable="true">Paint shopping plan</h1>
<div class="edit-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" fill="#a0a0a0"/>
</svg>
</div>
</div>
<div class="note-meta">
<span class="note-date">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 4H18V2H16V4H8V2H6V4H5C3.89 4 3.01 4.9 3.01 6L3 20C3 21.1 3.89 22 5 22H19C20.1 22 21 21.1 21 20V6C21 4.9 20.1 4 19 4ZM19 20H5V10H19V20ZM19 8H5V6H19V8ZM9 14H7V12H9V14ZM13 14H11V12H13V14ZM17 14H15V12H17V14ZM9 18H7V16H9V18ZM13 18H11V16H13V18ZM17 18H15V16H17V18Z" fill="#6947BD"/>
</svg>
<span id="noteDate">Apr 24</span>
</span>
<span class="note-author">
<svg width="14" height="14" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="20" fill="#f0f0f0"/>
<path d="M20 20C22.7614 20 25 17.7614 25 15C25 12.2386 22.7614 10 20 10C17.2386 10 15 12.2386 15 15C15 17.7614 17.2386 20 20 20Z" fill="#a0a0a0"/>
<path d="M12 31C12 26.0294 15.5817 22 20 22C24.4183 22 28 26.0294 28 31" stroke="#a0a0a0" stroke-width="4"/>
</svg>
Me
</span>
<a class="recall-link" id="recallLink" href="#" style="display: none;" title="View recording on Recall.ai">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" fill="currentColor"/>
</svg>
View Recording
</a>
</div>
</div>
<textarea id="simple-editor"># Meeting Title
• Paint shopping plan
# Meeting Date and Time
• April 24, 2025, 22:36:12
# Participants
• Nick Faro
# Description
• The meeting involved Nick Faro discussing plans to purchase a can of paint. The conversation was brief and included a humorous remark about going to Mars. There were no specific action items or further details discussed during the meeting.
Chat with meeting transcript: https://notes.granola.ai/d/393a95cf-4788-4a47-9deb-3952c665a56b</textarea>
</main>
<aside class="sidebar" id="sidebar" style="display:none;">
<div class="sidebar-content">
<section class="sidebar-section">
<h3>SHARE NOTES</h3>
<div class="share-options">
<button class="share-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.9 12C3.9 10.29 5.29 8.9 7 8.9H11V7H7C4.24 7 2 9.24 2 12C2 14.76 4.24 17 7 17H11V15.1H7C5.29 15.1 3.9 13.71 3.9 12ZM8 13H16V11H8V13ZM17 7H13V8.9H17C18.71 8.9 20.1 10.29 20.1 12C20.1 13.71 18.71 15.1 17 15.1H13V17H17C19.76 17 22 14.76 22 12C22 9.24 19.76 7 17 7Z" fill="#666666"/>
</svg>
Copy link
</button>
<button class="share-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM19 19H5V5H19V19ZM7 10H9V17H7V10ZM11 7H13V17H11V7ZM15 13H17V17H15V13Z" fill="#666666"/>
</svg>
Copy text
</button>
</div>
<div class="share-export">
<button class="share-btn wide">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 4H4C2.9 4 2.01 4.9 2.01 6L2 18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6C22 4.9 21.1 4 20 4ZM20 18H4V8L12 13L20 8V18ZM12 11L4 6H20L12 11Z" fill="#666666"/>
</svg>
Email
</button>
<button class="share-btn wide">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6ZM20 6L12 11L4 6H20ZM20 18H4V8L12 13L20 8V18Z" fill="#4285F4"/>
</svg>
Slack
</button>
</div>
</section>
<section class="sidebar-section">
<h3>ASK TWENTY</h3>
<div class="ai-options">
<button class="ai-btn">Generate meeting summary</button>
<button class="ai-btn">List action items</button>
<button class="ai-btn">Write follow-up email</button>
<button class="ai-btn">List Q&A</button>
</div>
</section>
</div>
</aside>
</div>
<div class="floating-controls">
<div class="control-buttons">
<button class="control-btn record-btn" id="recordButton">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="record-icon">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" fill="currentColor"/>
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" fill="currentColor"/>
</svg>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="stop-icon" style="display: none;">
<path d="M6 6h12v12H6V6z" fill="currentColor"/>
</svg>
</button>
<button class="control-btn generate-btn">
<svg width="16" height="16" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
<path d="M208,512a24.84,24.84,0,0,1-23.34-16l-39.84-103.6a16.06,16.06,0,0,0-9.19-9.19L32,343.34a25,25,0,0,1,0-46.68l103.6-39.84a16.06,16.06,0,0,0,9.19-9.19L184.66,144a25,25,0,0,1,46.68,0l39.84,103.6a16.06,16.06,0,0,0,9.19,9.19l103,39.63A25.49,25.49,0,0,1,400,320.52a24.82,24.82,0,0,1-16,22.82l-103.6,39.84a16.06,16.06,0,0,0-9.19,9.19L231.34,496A24.84,24.84,0,0,1,208,512Z" fill="currentColor"/>
<path d="M88,176a14.67,14.67,0,0,1-13.69-9.4L57.45,122.76a7.28,7.28,0,0,0-4.21-4.21L9.4,101.69a14.67,14.67,0,0,1,0-27.38L53.24,57.45a7.31,7.31,0,0,0,4.21-4.21L74.16,9.79A15,15,0,0,1,86.23.11,14.67,14.67,0,0,1,101.69,9.4l16.86,43.84a7.31,7.31,0,0,0,4.21,4.21L166.6,74.31a14.67,14.67,0,0,1,0,27.38l-43.84,16.86a7.28,7.28,0,0,0-4.21,4.21L101.69,166.6A14.67,14.67,0,0,1,88,176Z" fill="currentColor"/>
<path d="M400,256a16,16,0,0,1-14.93-10.26l-22.84-59.37a8,8,0,0,0-4.6-4.6l-59.37-22.84a16,16,0,0,1,0-29.86l59.37-22.84a8,8,0,0,0,4.6-4.6L384.9,42.68a16.45,16.45,0,0,1,13.17-10.57,16,16,0,0,1,16.86,10.15l22.84,59.37a8,8,0,0,0,4.6,4.6l59.37,22.84a16,16,0,0,1,0,29.86l-59.37,22.84a8,8,0,0,0-4.6,4.6l-22.84,59.37A16,16,0,0,1,400,256Z" fill="currentColor"/>
</svg>
Auto
</button>
</div>
</div>
<div class="chat-input-container">
<div class="chat-input">
<input type="text" id="chatInput" placeholder="Ask about the meeting..." style="display: none;" />
<button id="sendButton" style="display: none;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.01 21L23 12L2.01 3L2 10L17 12L2 14L2.01 21Z" fill="#0077FF"/>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Debug Panel -->
<div class="debug-panel hidden" id="debugPanel">
<div class="debug-panel-header" id="debug-drag-region">
<h3>Debugging Tools</h3>
<button class="debug-panel-close" id="closeDebugPanelBtn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
</svg>
</button>
</div>
<div class="debug-panel-content">
<!-- SDK Logger Section -->
<div class="debug-section" id="loggerSection">
<div class="debug-section-header">
<h4>RecallAI SDK Logger</h4>
<button class="debug-section-action" id="clearLoggerBtn">Clear</button>
</div>
<div class="debug-section-content" id="sdkLoggerContent">
<!-- Log entries will be added here dynamically -->
</div>
</div>
<!-- Transcript Section -->
<div class="debug-section" id="transcriptSection">
<div class="debug-section-header">
<h4>Transcript</h4>
</div>
<div class="debug-section-content" id="transcriptContent">
<div class="placeholder-content">
<p>Live transcript will appear here during recording</p>
</div>
</div>
</div>
<!-- Participants Section -->
<div class="debug-section" id="participantsSection">
<div class="debug-section-header">
<h4>Meeting Participants</h4>
</div>
<div class="debug-section-content" id="participantsContent">
<div class="placeholder-content">
<p>Detected participants will appear here</p>
</div>
</div>
</div>
<!-- Participant Video Debug Section -->
<div class="debug-section" id="videoSection">
<div class="debug-section-header">
<h4>Participant Video</h4>
</div>
<div class="debug-section-content" id="videoContent">
<div class="placeholder-content video-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" fill="#999"/>
</svg>
<p>Participant video will appear here</p>
</div>
</div>
</div>
<!-- Screenshare Video Debug Section -->
<div class="debug-section" id="screenshareSection">
<div class="debug-section-header">
<h4>Screenshare</h4>
</div>
<div class="debug-section-content" id="screenshareContent">
<div class="placeholder-content video-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 18c1.1 0 1.99-.9 1.99-2L22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z" fill="#999"/>
</svg>
<p>Screenshare will appear here</p>
</div>
</div>
</div>
</div>
</div>
<button class="debug-panel-toggle panel-hidden" id="debugPanelToggle">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z" fill="currentColor"/>
</svg>
</button>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -1,149 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Twenty - Note Editor</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
</head>
<body>
<div class="app-container">
<header class="header" id="drag-region">
<div class="header-left">
<button class="btn back-btn" id="backButton">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 11H7.83L13.42 5.41L12 4L4 12L12 20L13.41 18.59L7.83 13H20V11Z" fill="currentColor"/>
</svg>
</button>
<div class="app-logo">
<svg width="28" height="28" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="96" height="96" rx="11.293" fill="#000000"/>
<path d="M19.25 35.746C19.25 30.5 23.508 26.246 28.75 26.246H47.039C47.309 26.246 47.555 26.406 47.668 26.652C47.781 26.902 47.73 27.191 47.547 27.395L43.539 31.75C42.84 32.508 41.859 32.945 40.828 32.945H28.801C27.227 32.945 25.949 34.223 25.949 35.797V42.98C25.949 43.906 25.199 44.652 24.273 44.652H20.93C20.004 44.652 19.258 43.906 19.258 42.98V35.746Z" fill="white"/>
<path d="M76.152 60.254C76.152 65.5 71.895 69.754 66.648 69.754H58.879C53.633 69.754 49.375 65.5 49.375 60.254V46.652C49.375 45.727 49.723 44.836 50.352 44.152L54.883 39.234C55.074 39.027 55.371 38.957 55.637 39.055C55.898 39.164 56.074 39.41 56.074 39.691V60.211C56.074 61.785 57.352 63.063 58.926 63.063H66.605C68.18 63.063 69.457 61.785 69.457 60.211V35.797C69.457 34.223 68.18 32.945 66.605 32.945H57.676C56.652 32.945 55.68 33.375 54.98 34.121L28.348 63.063H44.352C45.273 63.063 46.023 63.813 46.023 64.738V68.082C46.023 69.008 45.273 69.754 44.352 69.754H22.785C20.832 69.754 19.242 68.168 19.242 66.211V64.441C19.242 63.551 19.574 62.695 20.18 62.039L50.039 29.605C52.016 27.457 54.789 26.246 57.707 26.246H66.641C71.887 26.246 76.145 30.5 76.145 35.746V60.254Z" fill="white"/>
</svg>
</div>
</div>
<div class="header-center">
<div class="search-container">
<span class="search-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5 14H14.71L14.43 13.73C15.41 12.59 16 11.11 16 9.5C16 5.91 13.09 3 9.5 3C5.91 3 3 5.91 3 9.5C3 13.09 5.91 16 9.5 16C11.11 16 12.59 15.41 13.73 14.43L14 14.71V15.5L19 20.49L20.49 19L15.5 14ZM9.5 14C7.01 14 5 11.99 5 9.5C5 7.01 7.01 5 9.5 5C11.99 5 14 7.01 14 9.5C14 11.99 11.99 14 9.5 14Z" fill="#666666"/>
</svg>
</span>
<input type="text" class="search-input" placeholder="Search">
</div>
</div>
<div class="header-right">
<button id="toggleSidebar" class="btn toggle-sidebar-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 18H21V16H3V18ZM3 13H21V11H3V13ZM3 6V8H21V6H3Z" fill="#666666"/>
</svg>
</button>
<div class="user-avatar">
<svg width="32" height="32" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="20" fill="#f0f0f0"/>
<path d="M20 20C22.7614 20 25 17.7614 25 15C25 12.2386 22.7614 10 20 10C17.2386 10 15 12.2386 15 15C15 17.7614 17.2386 20 20 20Z" fill="#a0a0a0"/>
<path d="M12 31C12 26.0294 15.5817 22 20 22C24.4183 22 28 26.0294 28 31" stroke="#a0a0a0" stroke-width="4"/>
</svg>
</div>
</div>
</header>
<div class="note-container">
<main class="editor-content">
<div class="note-header">
<h1 id="noteTitle">Paint shopping plan</h1>
<div class="note-meta">
<span class="note-date">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 4H18V2H16V4H8V2H6V4H5C3.89 4 3.01 4.9 3.01 6L3 20C3 21.1 3.89 22 5 22H19C20.1 22 21 21.1 21 20V6C21 4.9 20.1 4 19 4ZM19 20H5V10H19V20ZM19 8H5V6H19V8ZM9 14H7V12H9V14ZM13 14H11V12H13V14ZM17 14H15V12H17V14ZM9 18H7V16H9V18ZM13 18H11V16H13V18ZM17 18H15V16H17V18Z" fill="#6947BD"/>
</svg>
Apr 24
</span>
<span class="note-author">
<svg width="14" height="14" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="20" fill="#f0f0f0"/>
<path d="M20 20C22.7614 20 25 17.7614 25 15C25 12.2386 22.7614 10 20 10C17.2386 10 15 12.2386 15 15C15 17.7614 17.2386 20 20 20Z" fill="#a0a0a0"/>
<path d="M12 31C12 26.0294 15.5817 22 20 22C24.4183 22 28 26.0294 28 31" stroke="#a0a0a0" stroke-width="4"/>
</svg>
Me
</span>
</div>
</div>
<div id="editor">
# Meeting Title
* Paint shopping plan
# Meeting Date and Time
* April 24, 2025, 22:36:12
# Participants
* Nick Faro
# Description
* The meeting involved Nick Faro discussing plans to purchase a can of paint. The conversation was brief and included a humorous remark about going to Mars. There were no specific action items or further details discussed during the meeting.
Chat with meeting transcript: https://notes.granola.ai/d/393a95cf-4788-4a47-9deb-3952c665a56b
</div>
</main>
<aside class="sidebar" id="sidebar">
<div class="sidebar-content">
<section class="sidebar-section">
<h3>SHARE NOTES</h3>
<div class="share-options">
<button class="share-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.9 12C3.9 10.29 5.29 8.9 7 8.9H11V7H7C4.24 7 2 9.24 2 12C2 14.76 4.24 17 7 17H11V15.1H7C5.29 15.1 3.9 13.71 3.9 12ZM8 13H16V11H8V13ZM17 7H13V8.9H17C18.71 8.9 20.1 10.29 20.1 12C20.1 13.71 18.71 15.1 17 15.1H13V17H17C19.76 17 22 14.76 22 12C22 9.24 19.76 7 17 7Z" fill="#666666"/>
</svg>
Copy link
</button>
<button class="share-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM19 19H5V5H19V19ZM7 10H9V17H7V10ZM11 7H13V17H11V7ZM15 13H17V17H15V13Z" fill="#666666"/>
</svg>
Copy text
</button>
</div>
<div class="share-export">
<button class="share-btn wide">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 4H4C2.9 4 2.01 4.9 2.01 6L2 18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6C22 4.9 21.1 4 20 4ZM20 18H4V8L12 13L20 8V18ZM12 11L4 6H20L12 11Z" fill="#666666"/>
</svg>
Email
</button>
<button class="share-btn wide">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6ZM20 6L12 11L4 6H20ZM20 18H4V8L12 13L20 8V18Z" fill="#4285F4"/>
</svg>
Slack
</button>
</div>
</section>
<section class="sidebar-section">
<h3>ASK TWENTY</h3>
<div class="ai-options">
<button class="ai-btn">List action items</button>
<button class="ai-btn">Write follow-up email</button>
<button class="ai-btn">List Q&A</button>
</div>
</section>
</div>
</aside>
</div>
<div class="chat-input">
<input type="text" id="chatInput" placeholder="Ask about the meeting..." />
<button id="sendButton">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.01 21L23 12L2.01 3L2 10L17 12L2 14L2.01 21Z" fill="#0077FF"/>
</svg>
</button>
</div>
</div>
<script src="renderer.js"></script>
</body>
</html>
@@ -1,79 +0,0 @@
// Import styles
import './styles.css';
// Initialize the markdown editor when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Initialize SimpleMDE Markdown Editor
const editor = new SimpleMDE({
element: document.getElementById('editor'),
spellChecker: false,
autofocus: true,
status: false,
toolbar: [
'bold', 'italic', 'heading', '|',
'quote', 'unordered-list', 'ordered-list', '|',
'link', 'image', '|',
'preview', 'side-by-side', 'fullscreen',
],
placeholder: 'Write your notes here...',
initialValue: document.getElementById('editor').textContent.trim(),
});
// Handle sidebar toggle
const toggleSidebarBtn = document.getElementById('toggleSidebar');
const sidebar = document.getElementById('sidebar');
const editorContent = document.querySelector('.editor-content');
toggleSidebarBtn.addEventListener('click', () => {
sidebar.classList.toggle('hidden');
editorContent.classList.toggle('full-width');
});
// Handle back button
const backButton = document.getElementById('backButton');
backButton.addEventListener('click', () => {
window.electronAPI.navigate('home');
});
// Chat input handling
const chatInput = document.getElementById('chatInput');
const sendButton = document.getElementById('sendButton');
// When send button is clicked
sendButton.addEventListener('click', () => {
const message = chatInput.value.trim();
if (message) {
console.log('Sending message:', message);
// Here you would handle the AI chat functionality
// For now, just clear the input
chatInput.value = '';
}
});
// Send message on Enter key
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendButton.click();
}
});
// Handle share buttons
const shareButtons = document.querySelectorAll('.share-btn');
shareButtons.forEach(button => {
button.addEventListener('click', () => {
const action = button.textContent.trim();
console.log(`Share action: ${action}`);
// Implement actual sharing functionality here
});
});
// Handle AI option buttons
const aiButtons = document.querySelectorAll('.ai-btn');
aiButtons.forEach(button => {
button.addEventListener('click', () => {
const action = button.textContent.trim();
console.log(`AI action: ${action}`);
// Implement actual AI functionality here
});
});
});
@@ -1,356 +0,0 @@
:root {
--primary-bg: #f9f9f9;
--card-bg: #fff;
--light-purple: #f3e9ff;
--light-green: #e8f5e9;
--border-color: #e0e0e0;
--text-primary: #000;
--text-secondary: #666;
--sidebar-width: 320px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--primary-bg);
margin: 0;
overflow-x: hidden;
color: var(--text-primary);
}
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Header Styles */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: var(--card-bg);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
}
#drag-region {
-webkit-app-region: drag;
}
.header-left, .header-right {
display: flex;
align-items: center;
}
.header-left {
margin-left: 60px;
gap: 8px;
}
.app-logo {
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
}
.header-center {
flex-grow: 1;
max-width: 400px;
margin: 0 20px;
}
.search-container {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 10px;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
}
.search-input {
width: 100%;
padding: 8px 8px 8px 35px;
border-radius: 5px;
border: 1px solid var(--border-color);
background-color: #f2f2f2;
font-size: 14px;
-webkit-app-region: no-drag;
}
.btn {
padding: 8px 16px;
border-radius: 5px;
border: none;
cursor: pointer;
font-weight: 500;
-webkit-app-region: no-drag;
display: flex;
align-items: center;
justify-content: center;
}
.back-btn {
background-color: transparent;
color: var(--text-secondary);
padding: 8px;
}
.toggle-sidebar-btn {
background-color: transparent;
color: var(--text-secondary);
padding: 8px;
margin-right: 10px;
display: none;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
overflow: hidden;
background-color: #ddd;
-webkit-app-region: no-drag;
}
/* Note Editor Layout */
.note-container {
display: flex;
flex: 1;
position: relative;
}
.editor-content {
flex: 1;
padding: 40px;
background-color: var(--card-bg);
max-width: calc(100% - var(--sidebar-width));
transition: max-width 0.3s ease;
}
.editor-content.full-width {
max-width: 100%;
}
.note-header {
margin-bottom: 30px;
}
.note-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
}
.note-meta {
display: flex;
gap: 15px;
color: var(--text-secondary);
font-size: 14px;
margin-top: 10px;
}
.note-date, .note-author {
display: flex;
align-items: center;
gap: 5px;
}
/* Editor */
.CodeMirror, .editor-preview {
font-size: 16px;
line-height: 1.6;
}
.CodeMirror {
border: none;
height: calc(100vh - 230px);
}
.editor-toolbar {
border: none;
opacity: 0.8;
}
/* Sidebar */
.sidebar {
width: var(--sidebar-width);
background-color: var(--card-bg);
border-left: 1px solid var(--border-color);
padding: 20px;
overflow-y: auto;
transition: transform 0.3s ease;
}
.sidebar.hidden {
transform: translateX(var(--sidebar-width));
}
.sidebar-section {
margin-bottom: 30px;
}
.sidebar-section h3 {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 15px;
font-weight: 500;
}
.share-options, .share-export {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.share-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 5px;
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
flex: 1;
justify-content: center;
}
.share-btn.wide {
flex: 1;
}
.ai-options {
display: flex;
flex-direction: column;
gap: 10px;
}
.ai-btn {
padding: 10px 16px;
border-radius: 5px;
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
text-align: left;
}
/* Chat Input */
.chat-input {
display: flex;
margin: 20px auto;
width: 90%;
max-width: 800px;
position: relative;
bottom: 20px;
}
#chatInput {
flex: 1;
padding: 12px 20px;
border-radius: 20px;
border: 1px solid var(--border-color);
font-size: 14px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
#sendButton {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
cursor: pointer;
}
/* SimpleMDE customizations */
.CodeMirror-fullscreen, .editor-preview-side {
z-index: 999;
}
.editor-toolbar.fullscreen {
z-index: 1000;
}
/* Custom markdown rendering */
.editor-preview h1, .editor-preview-side h1 {
font-size: 22px;
color: #333;
margin: 25px 0 15px 0;
}
.editor-preview h2, .editor-preview-side h2 {
font-size: 18px;
color: #444;
margin: 20px 0 10px 0;
}
.editor-preview ul, .editor-preview-side ul {
padding-left: 20px;
margin: 10px 0;
}
.editor-preview a, .editor-preview-side a {
color: #0077ff;
text-decoration: none;
}
.editor-preview a:hover, .editor-preview-side a:hover {
text-decoration: underline;
}
/* Make code look nice */
.editor-preview code, .editor-preview-side code {
background-color: #f4f4f4;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
/* Mobile adjustments */
@media (max-width: 768px) {
.editor-content {
padding: 20px;
}
.sidebar {
width: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
z-index: 200;
transform: translateX(100%);
}
.sidebar.hidden {
transform: translateX(0);
}
.editor-content {
max-width: 100%;
}
}
-40
View File
@@ -1,40 +0,0 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
const { contextBridge, ipcRenderer } = require('electron');
// Set up the SDK logger bridge between main and renderer
contextBridge.exposeInMainWorld('sdkLoggerBridge', {
// Receive logs from main process
onSdkLog: (callback) => ipcRenderer.on('sdk-log', (_, logEntry) => callback(logEntry)),
// Send logs from renderer to main process
sendSdkLog: (logEntry) => ipcRenderer.send('sdk-log', logEntry)
});
contextBridge.exposeInMainWorld('electronAPI', {
navigate: (page) => ipcRenderer.send('navigate', page),
saveMeetingsData: (data) => ipcRenderer.invoke('saveMeetingsData', data),
loadMeetingsData: () => ipcRenderer.invoke('loadMeetingsData'),
deleteMeeting: (meetingId) => ipcRenderer.invoke('deleteMeeting', meetingId),
generateMeetingSummary: (meetingId) => ipcRenderer.invoke('generateMeetingSummary', meetingId),
generateMeetingSummaryStreaming: (meetingId) => ipcRenderer.invoke('generateMeetingSummaryStreaming', meetingId),
startManualRecording: (meetingId) => ipcRenderer.invoke('startManualRecording', meetingId),
stopManualRecording: (recordingId) => ipcRenderer.invoke('stopManualRecording', recordingId),
debugGetHandlers: () => ipcRenderer.invoke('debugGetHandlers'),
checkForDetectedMeeting: () => ipcRenderer.invoke('checkForDetectedMeeting'),
joinDetectedMeeting: () => ipcRenderer.invoke('joinDetectedMeeting'),
onOpenMeetingNote: (callback) => ipcRenderer.on('open-meeting-note', (_, meetingId) => callback(meetingId)),
onRecordingCompleted: (callback) => ipcRenderer.on('recording-completed', (_, meetingId) => callback(meetingId)),
onTranscriptUpdated: (callback) => ipcRenderer.on('transcript-updated', (_, meetingId) => callback(meetingId)),
onSummaryGenerated: (callback) => ipcRenderer.on('summary-generated', (_, meetingId) => callback(meetingId)),
onSummaryUpdate: (callback) => ipcRenderer.on('summary-update', (_, data) => callback(data)),
onRecordingStateChange: (callback) => ipcRenderer.on('recording-state-change', (_, data) => callback(data)),
onParticipantsUpdated: (callback) => ipcRenderer.on('participants-updated', (_, meetingId) => callback(meetingId)),
onVideoFrame: (callback) => ipcRenderer.on('video-frame', (_, data) => callback(data)),
onMeetingDetectionStatus: (callback) => ipcRenderer.on('meeting-detection-status', (_, data) => callback(data)),
onMeetingTitleUpdated: (callback) => ipcRenderer.on('meeting-title-updated', (_, data) => callback(data)),
getActiveRecordingId: (noteId) => ipcRenderer.invoke('getActiveRecordingId', noteId),
openExternal: (url) => ipcRenderer.invoke('openExternal', url),
getRecordingVideoUrl: (recordingId) => ipcRenderer.invoke('getRecordingVideoUrl', recordingId)
});
File diff suppressed because it is too large Load Diff
@@ -1,84 +0,0 @@
// SDK Logger for tracking RecallAI SDK operations
// This module is shared between main and renderer processes
// Create an event emitter for communication between processes
const { EventEmitter } = require('events');
const logger = new EventEmitter();
// Export the logger
module.exports = {
// Log an SDK API call
logApiCall: function(method, params = {}) {
const logEntry = {
type: 'api-call',
method,
params,
timestamp: new Date()
};
// Emit the log event
logger.emit('log', logEntry);
// Return a reference to the logger for chaining
return this;
},
// Log an SDK event
logEvent: function(eventType, data = {}) {
const logEntry = {
type: 'event',
eventType,
data,
timestamp: new Date()
};
// Emit the log event
logger.emit('log', logEntry);
// Return a reference to the logger for chaining
return this;
},
// Log an error
logError: function(errorType, message) {
const logEntry = {
type: 'error',
errorType,
message,
timestamp: new Date()
};
// Emit the log event
logger.emit('log', logEntry);
// Return a reference to the logger for chaining
return this;
},
// Log a generic message
log: function(message, level = 'info') {
const logEntry = {
type: level,
message,
timestamp: new Date()
};
// Emit the log event
logger.emit('log', logEntry);
// Return a reference to the logger for chaining
return this;
},
// Set up a listener for logs
onLog: function(callback) {
logger.on('log', callback);
return this;
},
// Remove a log listener
removeLogListener: function(callback) {
logger.off('log', callback);
return this;
}
};
-104
View File
@@ -1,104 +0,0 @@
const express = require('express');
const axios = require('axios');
const { z } = require('zod');
const app = express();
require('dotenv').config();
// API configuration for Recall.ai
const RECALLAI_API_URL = process.env.RECALLAI_API_URL || 'https://api.recall.ai';
const RECALLAI_API_KEY = process.env.RECALLAI_API_KEY;
app.get('/start-recording', async (req, res) => {
console.log('Creating upload token with configured Recall.ai API key');
if (!RECALLAI_API_KEY) {
console.error("RECALLAI_API_KEY is missing! Set it in .env file");
return res.json({ status: 'error', message: 'RECALLAI_API_KEY is missing' });
}
const url = `${RECALLAI_API_URL}/api/v1/sdk_upload/`;
try {
const response = await axios.post(url, {
recording_config: {
transcript: {
provider: {
assembly_ai_v3_streaming: {}
}
},
realtime_endpoints: [
{
type: "desktop_sdk_callback",
events: [
"participant_events.join",
"video_separate_png.data",
"transcript.data",
"transcript.provider_data"
]
},
],
}
}, {
headers: { 'Authorization': `Token ${RECALLAI_API_KEY}` },
timeout: 9000,
});
res.json({
status: 'success',
upload_token: response.data.upload_token,
upload_id: response.data.id,
recording_id: response.data.recording_id,
});
} catch (e) {
console.error("Error creating upload token:", JSON.stringify(e.errors || e.response?.data || e.message));
res.json({ status: 'error', message: e.message });
}
});
app.get('/recording/:recordingId', async (req, res) => {
const parseResult = z.string().uuid().safeParse(req.params.recordingId);
if (!RECALLAI_API_KEY) {
return res.json({ status: 'error', message: 'RECALLAI_API_KEY is missing' });
}
if (!parseResult.success) {
return res.status(400).json({ status: 'error', message: 'Invalid recording ID' });
}
const validatedRecordingId = parseResult.data;
try {
const response = await axios.get(
`${RECALLAI_API_URL}/api/v1/recording/${validatedRecordingId}/`,
{
headers: { 'Authorization': `Token ${RECALLAI_API_KEY}` },
timeout: 10000,
}
);
const data = response.data;
const videoUrl = data.media_shortcuts?.video_mixed?.data?.download_url || null;
const transcriptUrl = data.media_shortcuts?.transcript?.data?.download_url || null;
const statusCode = data.status?.code || 'unknown';
res.json({
status: 'success',
recording_status: statusCode,
video_url: videoUrl,
transcript_url: transcriptUrl,
});
} catch (e) {
console.error("Error fetching recording:", JSON.stringify(e.response?.data || e.message));
res.json({ status: 'error', message: e.response?.data || e.message });
}
});
if (require.main === module) {
app.listen(13373, () => {
console.log(`Server listening on http://localhost:13373`);
});
}
module.exports = app;
@@ -1,119 +0,0 @@
const axios = require('axios');
const LOG_PREFIX = '[Twenty]';
let configLogged = false;
function getApiUrl() {
return process.env.TWENTY_API_URL;
}
function getWorkspaceSubdomain() {
return process.env.TWENTY_WORKSPACE_SUBDOMAIN;
}
function getApiKey() {
return process.env.TWENTY_API_KEY;
}
function isConfigured() {
const configured = Boolean(getApiUrl() && getApiKey());
if (!configLogged) {
configLogged = true;
if (!configured) {
console.log(
`${LOG_PREFIX} Integration disabled — TWENTY_API_URL or TWENTY_API_KEY not set`,
);
} else {
console.log(
`${LOG_PREFIX} Integration enabled — ${getApiUrl()}`,
);
}
}
return configured;
}
function getClient() {
return axios.create({
baseURL: `${getApiUrl()}/rest`,
headers: {
Authorization: `Bearer ${getApiKey()}`,
'Content-Type': 'application/json',
},
timeout: 10000,
});
}
function logError(action, error) {
const status = error.response?.status;
const body = error.response?.data;
console.error(
`${LOG_PREFIX} ${action} failed —`,
status ? `HTTP ${status}` : error.code || error.message,
body ? JSON.stringify(body) : '',
);
}
async function createCallRecording(name) {
console.log(`${LOG_PREFIX} Creating callRecording: "${name}"`);
const client = getClient();
try {
const response = await client.post('/callRecordings', {
name,
createdAt: new Date().toISOString(),
});
const record = response.data?.data?.createCallRecording;
console.log(
`${LOG_PREFIX} callRecording created — id=${record?.id}`,
);
return record;
} catch (error) {
logError('createCallRecording', error);
throw error;
}
}
async function endCallRecording({ callRecordingId, audioUrl, transcriptUrl, participants, localTranscript }) {
console.log(
`${LOG_PREFIX} Ending callRecording ${callRecordingId} — audioUrl=${audioUrl} transcriptUrl=${transcriptUrl || 'none'} participants=${participants?.length || 0} localTranscriptEntries=${localTranscript?.length || 0}`,
);
try {
const apiUrl = new URL(getApiUrl());
const subdomain = getWorkspaceSubdomain();
const host = subdomain
? `${subdomain}.${apiUrl.hostname}:${apiUrl.port}`
: apiUrl.host;
const response = await axios.post(
`${getApiUrl()}/s/end-recording`,
{ callRecordingId, audioUrl, transcriptUrl, participants, localTranscript },
{
headers: {
'Content-Type': 'application/json',
Host: host,
},
timeout: 30000,
},
);
console.log(
`${LOG_PREFIX} callRecording ended — id=${callRecordingId}`,
);
return response.data;
} catch (error) {
logError('endCallRecording', error);
throw error;
}
}
module.exports = { isConfigured, createCallRecording, endCallRecording };
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,14 +0,0 @@
module.exports = {
/**
* This is the main entry point for your application, it's the first file
* that runs in the main process.
*/
entry: './src/main.js',
// Put your normal webpack config below here
module: {
rules: require('./webpack.rules'),
},
externals: {
'@recallai/desktop-sdk': 'commonjs @recallai/desktop-sdk'
}
};
@@ -1,17 +0,0 @@
const rules = require('./webpack.rules');
rules.push({
test: /\.css$/,
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
});
module.exports = {
// Put your normal webpack config below here
module: {
rules,
},
entry: {
renderer: './src/renderer.js',
'note-editor/renderer': './src/pages/note-editor/renderer.js',
},
};
@@ -1,17 +0,0 @@
module.exports = [
// Add support for native node modules
{
// We're specifying native_modules in the test because the asset relocator loader generates a
// "fake" .node file which is really a cjs file.
test: /native_modules[/\\].+\.node$/,
use: 'node-loader',
},
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset/inline',
},
{
test: /\.svg$/i,
type: 'asset/resource',
},
];
+169 -3995
View File
File diff suppressed because it is too large Load Diff