feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
Executable
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ExtensionList>
|
||||
<Extension Id="com.ppro.ai.chat.panel">
|
||||
<HostList>
|
||||
<Host Name="PPRO" Port="8098"/>
|
||||
</HostList>
|
||||
</Extension>
|
||||
</ExtensionList>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**************************************************************************************************
|
||||
* ADOBE SYSTEMS INCORPORATED
|
||||
* Copyright 2013 Adobe Systems Incorporated
|
||||
* All Rights Reserved.
|
||||
*
|
||||
* NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the
|
||||
* terms of the Adobe license agreement accompanying it. If you have received this file from a
|
||||
* source other than Adobe, then your use, modification, or distribution of it requires the prior
|
||||
* written permission of Adobe.
|
||||
*
|
||||
* CSInterface.js - v12.0.0 (minimal shim for MCP Bridge)
|
||||
* Download the full version from: https://github.com/nicscott9/CSInterface
|
||||
**************************************************************************************************/
|
||||
|
||||
/**
|
||||
* CSInterface class for Adobe CEP extensions.
|
||||
* This is a minimal implementation. For production use, download the full
|
||||
* CSInterface.js from Adobe's GitHub repository.
|
||||
*/
|
||||
function CSInterface() {}
|
||||
|
||||
/**
|
||||
* Evaluates an ExtendScript in the host application.
|
||||
* @param {string} script - The ExtendScript to evaluate.
|
||||
* @param {function} callback - Callback with the result string.
|
||||
*/
|
||||
CSInterface.prototype.evalScript = function (script, callback) {
|
||||
if (typeof __adobe_cep__ !== "undefined") {
|
||||
var result = __adobe_cep__.evalScript(script);
|
||||
if (callback) {
|
||||
// CSInterface v9+ uses async callback
|
||||
if (typeof result === "undefined" || result === "undefined") {
|
||||
// v9+ path: callback is registered and called asynchronously
|
||||
// The __adobe_cep__.evalScript already handles the callback via internal mechanism
|
||||
}
|
||||
callback(result);
|
||||
}
|
||||
} else {
|
||||
// Running outside CEP (for testing)
|
||||
console.warn("[CSInterface] Not running in CEP environment");
|
||||
if (callback) callback("EvalScript Error: Not in CEP environment");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the host environment.
|
||||
*/
|
||||
CSInterface.prototype.getHostEnvironment = function () {
|
||||
if (typeof __adobe_cep__ !== "undefined") {
|
||||
try {
|
||||
return JSON.parse(__adobe_cep__.getHostEnvironment());
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the system path.
|
||||
* @param {string} pathType - The path type constant.
|
||||
*/
|
||||
CSInterface.prototype.getSystemPath = function (pathType) {
|
||||
if (typeof __adobe_cep__ !== "undefined") {
|
||||
return __adobe_cep__.getSystemPath(pathType);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// System path constants
|
||||
CSInterface.prototype.EXTENSION_ID = "extensionId";
|
||||
|
||||
// Note: This is a minimal shim. For the full CSInterface.js, download from:
|
||||
// https://github.com/nicscott9/CSInterface
|
||||
// and replace this file with the appropriate version for your CEP target.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ExtensionManifest Version="7.0" ExtensionBundleId="com.ppro.ai.chat" ExtensionBundleVersion="1.0.0" ExtensionBundleName="Premiere Pro AI Chat">
|
||||
<ExtensionList>
|
||||
<Extension Id="com.ppro.ai.chat.panel" Version="1.0.0"/>
|
||||
</ExtensionList>
|
||||
<ExecutionEnvironment>
|
||||
<HostList>
|
||||
<Host Name="PPRO" Version="[14.0,99.9]"/>
|
||||
</HostList>
|
||||
<LocaleList>
|
||||
<Locale Code="All"/>
|
||||
</LocaleList>
|
||||
<RequiredRuntimeList>
|
||||
<RequiredRuntime Name="CSXS" Version="9.0"/>
|
||||
</RequiredRuntimeList>
|
||||
</ExecutionEnvironment>
|
||||
<DispatchInfoList>
|
||||
<Extension Id="com.ppro.ai.chat.panel">
|
||||
<DispatchInfo>
|
||||
<Resources>
|
||||
<MainPath>./index.html</MainPath>
|
||||
<ScriptPath>./host.jsx</ScriptPath>
|
||||
<CEFCommandLine>
|
||||
<Parameter>--allow-file-access-from-files</Parameter>
|
||||
<Parameter>--mixed-context</Parameter>
|
||||
</CEFCommandLine>
|
||||
</Resources>
|
||||
<Lifecycle>
|
||||
<AutoVisible>true</AutoVisible>
|
||||
</Lifecycle>
|
||||
<UI>
|
||||
<Type>Panel</Type>
|
||||
<Menu>AI Chat</Menu>
|
||||
<Geometry>
|
||||
<Size>
|
||||
<Height>600</Height>
|
||||
<Width>420</Width>
|
||||
</Size>
|
||||
<MinSize>
|
||||
<Height>400</Height>
|
||||
<Width>320</Width>
|
||||
</MinSize>
|
||||
<MaxSize>
|
||||
<Height>2000</Height>
|
||||
<Width>1200</Width>
|
||||
</MaxSize>
|
||||
</Geometry>
|
||||
<Icons/>
|
||||
</UI>
|
||||
</DispatchInfo>
|
||||
</Extension>
|
||||
</DispatchInfoList>
|
||||
</ExtensionManifest>
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
/* AI Provider Abstraction Layer
|
||||
* Supports Claude (Anthropic) and Gemini (Google) APIs.
|
||||
* Runs inside CEP (Chromium with Node.js access). */
|
||||
|
||||
var https = require("https");
|
||||
|
||||
// Track the current in-flight request so we can abort it
|
||||
var _currentRequest = null;
|
||||
|
||||
// ---- Provider Configurations ----
|
||||
var PROVIDERS = {
|
||||
claude: {
|
||||
name: "Claude",
|
||||
icon: "◆",
|
||||
keyHint: "Get a key at console.anthropic.com",
|
||||
keyUrl: "https://console.anthropic.com/settings/keys",
|
||||
models: [
|
||||
{ id: "claude-sonnet-4-20250514", label: "Claude Sonnet 4 (Best)" },
|
||||
{ id: "claude-3-5-sonnet-20241022", label: "Claude 3.5 Sonnet" },
|
||||
{ id: "claude-3-5-haiku-20241022", label: "Claude 3.5 Haiku (Fast)" },
|
||||
{ id: "claude-3-opus-20240229", label: "Claude 3 Opus" },
|
||||
],
|
||||
defaultModel: "claude-sonnet-4-20250514",
|
||||
},
|
||||
gemini: {
|
||||
name: "Gemini",
|
||||
icon: "✦",
|
||||
keyHint: "Get a key at aistudio.google.com",
|
||||
keyUrl: "https://aistudio.google.com/apikey",
|
||||
models: [
|
||||
{ id: "gemini-2.5-flash-preview-05-20", label: "Gemini 2.5 Flash (Best)" },
|
||||
{ id: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
|
||||
{ id: "gemini-1.5-pro", label: "Gemini 1.5 Pro" },
|
||||
{ id: "gemini-1.5-flash", label: "Gemini 1.5 Flash (Fast)" },
|
||||
],
|
||||
defaultModel: "gemini-2.5-flash-preview-05-20",
|
||||
},
|
||||
};
|
||||
|
||||
// ---- System Prompt ----
|
||||
var BASE_SYSTEM_PROMPT =
|
||||
"You are an AI assistant embedded inside Adobe Premiere Pro. " +
|
||||
"You can control Premiere Pro by generating ExtendScript code that runs directly in the application.\n\n" +
|
||||
"IMPORTANT RULES:\n" +
|
||||
"1. ExtendScript uses ES3 syntax only: use 'var' (never let/const), no arrow functions, no template literals, no destructuring.\n" +
|
||||
"2. Always wrap your scripts in a try/catch and return results via the __result() and __error() helper functions that are available globally.\n" +
|
||||
"3. Available helper functions: __ticksToSeconds(ticks), __secondsToTicks(seconds), __jsonStringify(obj), __result(data), __error(msg).\n" +
|
||||
"4. The app object is the global Premiere Pro application object.\n" +
|
||||
"5. To access the active sequence: var seq = app.project.activeSequence;\n" +
|
||||
"6. To access project items: app.project.rootItem.children\n" +
|
||||
"7. For QE DOM (advanced): call app.enableQE() first, then use qe.project, qe.source, etc.\n\n" +
|
||||
"When the user asks you to do something in Premiere Pro:\n" +
|
||||
"1. Explain what you will do briefly.\n" +
|
||||
"2. Generate the ExtendScript code in a ```extendscript code block.\n" +
|
||||
"3. The code will be automatically executed. You'll see the result and can follow up.\n\n" +
|
||||
"When the user asks a question about their project, generate ExtendScript to query the information.\n" +
|
||||
"Always be concise and helpful. If an operation fails, explain why and suggest alternatives.";
|
||||
|
||||
// ---- Claude (Anthropic) API ----
|
||||
function callClaude(apiKey, model, messages, systemPrompt, options, callback) {
|
||||
var body = JSON.stringify({
|
||||
model: model,
|
||||
max_tokens: options.maxTokens || 4096,
|
||||
temperature: typeof options.temperature === "number" ? options.temperature : 0.3,
|
||||
system: systemPrompt,
|
||||
messages: messages.map(function (m) {
|
||||
return { role: m.role, content: m.content };
|
||||
}),
|
||||
});
|
||||
|
||||
var reqOptions = {
|
||||
hostname: "api.anthropic.com",
|
||||
path: "/v1/messages",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
},
|
||||
};
|
||||
|
||||
makeRequest(reqOptions, body, function (err, data) {
|
||||
if (err) return callback(err, null);
|
||||
try {
|
||||
var parsed = JSON.parse(data);
|
||||
if (parsed.error) {
|
||||
return callback(parsed.error.message || "API error", null);
|
||||
}
|
||||
var text = "";
|
||||
if (parsed.content && parsed.content.length > 0) {
|
||||
for (var i = 0; i < parsed.content.length; i++) {
|
||||
if (parsed.content[i].type === "text") {
|
||||
text += parsed.content[i].text;
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(null, {
|
||||
text: text,
|
||||
usage: parsed.usage || {},
|
||||
model: parsed.model,
|
||||
stopReason: parsed.stop_reason,
|
||||
});
|
||||
} catch (e) {
|
||||
callback("Failed to parse response: " + e.message, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Gemini (Google) API ----
|
||||
function callGemini(apiKey, model, messages, systemPrompt, options, callback) {
|
||||
var contents = messages.map(function (m) {
|
||||
return {
|
||||
role: m.role === "assistant" ? "model" : "user",
|
||||
parts: [{ text: m.content }],
|
||||
};
|
||||
});
|
||||
|
||||
var body = JSON.stringify({
|
||||
contents: contents,
|
||||
systemInstruction: {
|
||||
parts: [{ text: systemPrompt }],
|
||||
},
|
||||
generationConfig: {
|
||||
temperature: typeof options.temperature === "number" ? options.temperature : 0.3,
|
||||
maxOutputTokens: options.maxTokens || 4096,
|
||||
},
|
||||
});
|
||||
|
||||
var reqOptions = {
|
||||
hostname: "generativelanguage.googleapis.com",
|
||||
path: "/v1beta/models/" + model + ":generateContent",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-goog-api-key": apiKey,
|
||||
},
|
||||
};
|
||||
|
||||
makeRequest(reqOptions, body, function (err, data) {
|
||||
if (err) return callback(err, null);
|
||||
try {
|
||||
var parsed = JSON.parse(data);
|
||||
if (parsed.error) {
|
||||
return callback(parsed.error.message || "API error", null);
|
||||
}
|
||||
var text = "";
|
||||
if (
|
||||
parsed.candidates &&
|
||||
parsed.candidates[0] &&
|
||||
parsed.candidates[0].content
|
||||
) {
|
||||
var parts = parsed.candidates[0].content.parts;
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i].text) text += parts[i].text;
|
||||
}
|
||||
}
|
||||
callback(null, {
|
||||
text: text,
|
||||
usage: parsed.usageMetadata || {},
|
||||
model: model,
|
||||
stopReason:
|
||||
parsed.candidates &&
|
||||
parsed.candidates[0] &&
|
||||
parsed.candidates[0].finishReason,
|
||||
});
|
||||
} catch (e) {
|
||||
callback("Failed to parse response: " + e.message, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Unified Call ----
|
||||
function callAI(provider, apiKey, model, messages, systemPrompt, options, callback) {
|
||||
var fullSystemPrompt = BASE_SYSTEM_PROMPT;
|
||||
if (systemPrompt) {
|
||||
fullSystemPrompt += "\n\n" + systemPrompt;
|
||||
}
|
||||
|
||||
if (provider === "claude") {
|
||||
callClaude(apiKey, model, messages, fullSystemPrompt, options, callback);
|
||||
} else if (provider === "gemini") {
|
||||
callGemini(apiKey, model, messages, fullSystemPrompt, options, callback);
|
||||
} else {
|
||||
callback("Unknown provider: " + provider, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Validate API Key (quick test call) ----
|
||||
function validateApiKey(provider, apiKey, model, callback) {
|
||||
var testMessages = [{ role: "user", content: "Reply with just the word: connected" }];
|
||||
callAI(provider, apiKey, model, testMessages, "", { maxTokens: 32 }, function (err, result) {
|
||||
if (err) return callback(false, err);
|
||||
if (result && result.text) return callback(true, null);
|
||||
callback(false, "No response received");
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Abort any in-flight request ----
|
||||
function abortCurrentRequest() {
|
||||
if (_currentRequest) {
|
||||
try { _currentRequest.destroy(); } catch (e) {}
|
||||
_currentRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- HTTPS Request Helper (Node.js) ----
|
||||
function makeRequest(options, body, callback) {
|
||||
abortCurrentRequest();
|
||||
|
||||
// Set Content-Length for compatibility with proxies/firewalls
|
||||
var bodyBuffer = Buffer.from(body, "utf-8");
|
||||
options.headers = options.headers || {};
|
||||
options.headers["Content-Length"] = bodyBuffer.length;
|
||||
|
||||
var req = https.request(options, function (res) {
|
||||
var chunks = [];
|
||||
res.on("data", function (chunk) {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
res.on("end", function () {
|
||||
var data = Buffer.concat(chunks).toString("utf-8");
|
||||
if (res.statusCode >= 400) {
|
||||
try {
|
||||
var errData = JSON.parse(data);
|
||||
var errMsg =
|
||||
(errData.error && errData.error.message) || "HTTP " + res.statusCode;
|
||||
callback(errMsg, null);
|
||||
} catch (e) {
|
||||
callback("HTTP " + res.statusCode + ": " + data.substring(0, 200), null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
callback(null, data);
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", function (e) {
|
||||
callback("Network error: " + e.message, null);
|
||||
});
|
||||
|
||||
req.setTimeout(60000, function () {
|
||||
req.destroy();
|
||||
callback("Request timed out (60s)", null);
|
||||
});
|
||||
|
||||
_currentRequest = req;
|
||||
req.write(bodyBuffer);
|
||||
req.end();
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
// Host-side ExtendScript (runs in Premiere Pro's ExtendScript engine)
|
||||
// These helpers are always available to the AI Chat panel.
|
||||
|
||||
var TICKS_PER_SECOND = 254016000000;
|
||||
|
||||
function __ticksToSeconds(ticks) {
|
||||
return parseFloat(ticks) / TICKS_PER_SECOND;
|
||||
}
|
||||
|
||||
function __secondsToTicks(seconds) {
|
||||
return Math.round(parseFloat(seconds) * TICKS_PER_SECOND);
|
||||
}
|
||||
|
||||
function __jsonStringify(obj) {
|
||||
if (typeof JSON !== "undefined" && JSON.stringify) {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
if (obj === null) return "null";
|
||||
if (obj === undefined) return "undefined";
|
||||
if (typeof obj === "string") return '"' + obj.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") + '"';
|
||||
if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
|
||||
if (obj instanceof Array) {
|
||||
var arr = [];
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
arr.push(__jsonStringify(obj[i]));
|
||||
}
|
||||
return "[" + arr.join(",") + "]";
|
||||
}
|
||||
if (typeof obj === "object") {
|
||||
var parts = [];
|
||||
for (var k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
parts.push(__jsonStringify(k) + ":" + __jsonStringify(obj[k]));
|
||||
}
|
||||
}
|
||||
return "{" + parts.join(",") + "}";
|
||||
}
|
||||
return String(obj);
|
||||
}
|
||||
|
||||
function __result(data) {
|
||||
return __jsonStringify({ success: true, data: data });
|
||||
}
|
||||
|
||||
function __error(msg) {
|
||||
return __jsonStringify({ success: false, error: String(msg) });
|
||||
}
|
||||
|
||||
function aiChatPing() {
|
||||
try {
|
||||
var version = app.version;
|
||||
var projectName = app.project && app.project.name ? app.project.name : "No project open";
|
||||
return __result({
|
||||
connected: true,
|
||||
premiereVersion: version,
|
||||
projectName: projectName
|
||||
});
|
||||
} catch(e) {
|
||||
return __error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectContext() {
|
||||
try {
|
||||
var project = app.project;
|
||||
if (!project) return __result({ hasProject: false });
|
||||
|
||||
var info = {
|
||||
hasProject: true,
|
||||
name: project.name,
|
||||
path: project.path,
|
||||
numSequences: project.sequences.numSequences,
|
||||
numItems: project.rootItem.children.numItems,
|
||||
activeSequence: null
|
||||
};
|
||||
|
||||
var seq = project.activeSequence;
|
||||
if (seq) {
|
||||
info.activeSequence = {
|
||||
name: seq.name,
|
||||
id: seq.sequenceID,
|
||||
videoTracks: seq.videoTracks.numTracks,
|
||||
audioTracks: seq.audioTracks.numTracks,
|
||||
frameSizeH: seq.frameSizeHorizontal,
|
||||
frameSizeV: seq.frameSizeVertical
|
||||
};
|
||||
}
|
||||
|
||||
return __result(info);
|
||||
} catch(e) {
|
||||
return __error(e.toString());
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>AI Chat — Premiere Pro</title>
|
||||
<link rel="stylesheet" href="styles.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ========== LOGIN SCREEN ========== -->
|
||||
<div id="loginScreen" class="screen">
|
||||
<div class="login-container">
|
||||
<div class="logo">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<rect width="48" height="48" rx="12" fill="#7C3AED"/>
|
||||
<path d="M14 34V14h6.5c2 0 3.6.5 4.8 1.6 1.2 1 1.8 2.5 1.8 4.3 0 1.3-.3 2.4-1 3.3-.7.9-1.6 1.5-2.7 1.8l4.6 9H24l-4.2-8.4H18V34h-4zm4-12.2h2.3c.9 0 1.6-.2 2.1-.7.5-.5.8-1.1.8-1.9s-.3-1.4-.8-1.9c-.5-.5-1.2-.7-2.1-.7H18v5.2z" fill="white"/>
|
||||
<circle cx="36" cy="14" r="6" fill="#22D3EE"/>
|
||||
<path d="M33.5 14l1.5 1.5 3-3" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>Premiere Pro AI Chat</h1>
|
||||
<p class="subtitle">Connect an AI provider to control Premiere Pro with natural language.</p>
|
||||
|
||||
<div class="provider-tabs">
|
||||
<button class="tab active" data-provider="claude" onclick="selectProvider('claude')">
|
||||
<span class="tab-icon">◆</span> Claude
|
||||
</button>
|
||||
<button class="tab" data-provider="gemini" onclick="selectProvider('gemini')">
|
||||
<span class="tab-icon">✦</span> Gemini
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="apiKeyInput">API Key</label>
|
||||
<div class="input-row">
|
||||
<input type="password" id="apiKeyInput" placeholder="Enter your API key..." autocomplete="off"/>
|
||||
<button class="icon-btn" onclick="toggleKeyVisibility()" title="Show/hide key">
|
||||
<span id="eyeIcon">👁</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="hint" id="providerHint">Get a key at <a href="#" id="providerLink" onclick="openLink(this.dataset.url)">console.anthropic.com</a></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="modelSelect">Model</label>
|
||||
<select id="modelSelect"></select>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" id="loginBtn" onclick="login()">Connect & Start Chatting</button>
|
||||
|
||||
<div id="loginError" class="error-msg" style="display:none"></div>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>Your API key is kept in memory only for this panel session.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== CHAT SCREEN ========== -->
|
||||
<div id="chatScreen" class="screen" style="display:none">
|
||||
<!-- Header -->
|
||||
<div class="chat-header">
|
||||
<div class="header-left">
|
||||
<div class="header-dot connected"></div>
|
||||
<span class="header-title" id="headerTitle">Claude</span>
|
||||
<span class="header-model" id="headerModel">claude-sonnet-4-20250514</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="icon-btn small" onclick="clearChat()" title="Clear chat">🗑</button>
|
||||
<button class="icon-btn small" onclick="openSettings()" title="Settings">⚙</button>
|
||||
<button class="icon-btn small" onclick="logout()" title="Disconnect">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Context Banner -->
|
||||
<div class="context-banner" id="contextBanner" style="display:none">
|
||||
<span class="context-icon">🎬</span>
|
||||
<span id="contextText">No project open</span>
|
||||
<button class="icon-btn tiny" onclick="refreshContext()" title="Refresh">↻</button>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="messages" id="messages">
|
||||
<div class="welcome-msg">
|
||||
<p><strong>Welcome!</strong> I can help you edit in Premiere Pro. Try:</p>
|
||||
<div class="suggestions">
|
||||
<button class="suggestion" onclick="sendSuggestion('What clips are in my timeline?')">What clips are in my timeline?</button>
|
||||
<button class="suggestion" onclick="sendSuggestion('Add a cross dissolve to all cuts')">Add a cross dissolve to all cuts</button>
|
||||
<button class="suggestion" onclick="sendSuggestion('Export the active sequence as H.264')">Export as H.264</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<div class="input-area">
|
||||
<div class="input-row">
|
||||
<textarea id="chatInput" placeholder="Ask me to edit your project..." rows="1" onkeydown="handleInputKey(event)" oninput="autoResizeInput()"></textarea>
|
||||
<button class="send-btn" id="sendBtn" onclick="sendMessage()" title="Send">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-footer">
|
||||
<span id="statusText">Ready</span>
|
||||
<span id="tokenCount"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== SETTINGS MODAL ========== -->
|
||||
<div id="settingsModal" class="modal" style="display:none">
|
||||
<div class="modal-backdrop" onclick="closeSettings()"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Settings</h2>
|
||||
<button class="icon-btn" onclick="closeSettings()">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="settingsModel">Model</label>
|
||||
<select id="settingsModel"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settingsTemp">Temperature</label>
|
||||
<input type="range" id="settingsTemp" min="0" max="1" step="0.1" value="0.3"/>
|
||||
<span id="settingsTempVal">0.3</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settingsMaxTokens">Max Tokens</label>
|
||||
<input type="number" id="settingsMaxTokens" value="4096" min="256" max="32000" step="256"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settingsSystemPrompt">System Prompt (appended to default)</label>
|
||||
<textarea id="settingsSystemPrompt" rows="4" placeholder="Add custom instructions..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="settingsAutoExec" checked/>
|
||||
Auto-execute ExtendScript (uncheck to preview first)
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<button class="btn-secondary" onclick="changeApiKey()">Change API Key</button>
|
||||
</div>
|
||||
<button class="btn-primary" onclick="saveSettings()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="CSInterface.js"></script>
|
||||
<script src="ai-providers.js"></script>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+661
@@ -0,0 +1,661 @@
|
||||
/* Premiere Pro AI Chat — Main Panel Logic
|
||||
* Handles UI state, chat flow, ExtendScript execution, and settings. */
|
||||
|
||||
var cs = new CSInterface();
|
||||
|
||||
// ---- Constants ----
|
||||
var MAX_HISTORY = 50; // Cap conversation history to prevent token overflow
|
||||
|
||||
// ---- State ----
|
||||
var state = {
|
||||
provider: "claude",
|
||||
apiKey: "",
|
||||
model: "",
|
||||
messages: [], // { role: "user"|"assistant", content: string }
|
||||
isStreaming: false,
|
||||
autoExec: true,
|
||||
temperature: 0.3,
|
||||
maxTokens: 4096,
|
||||
customSystemPrompt: "",
|
||||
projectContext: null,
|
||||
scriptQueue: [], // Sequential script execution queue
|
||||
scriptRunning: false,
|
||||
};
|
||||
|
||||
// ---- Provider Selection (Login Screen) ----
|
||||
function selectProvider(provider) {
|
||||
state.provider = provider;
|
||||
var tabs = document.querySelectorAll(".tab");
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
tabs[i].classList.toggle("active", tabs[i].dataset.provider === provider);
|
||||
}
|
||||
updateProviderUI();
|
||||
}
|
||||
|
||||
function updateProviderUI() {
|
||||
var config = PROVIDERS[state.provider];
|
||||
var hint = document.getElementById("providerHint");
|
||||
var link = document.getElementById("providerLink");
|
||||
hint.innerHTML = "Get a key at <a href=\"#\" id=\"providerLink\" onclick=\"openLink('" + config.keyUrl + "')\">" + config.keyUrl.replace("https://", "") + "</a>";
|
||||
|
||||
var select = document.getElementById("modelSelect");
|
||||
select.innerHTML = "";
|
||||
for (var i = 0; i < config.models.length; i++) {
|
||||
var opt = document.createElement("option");
|
||||
opt.value = config.models[i].id;
|
||||
opt.textContent = config.models[i].label;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
select.value = config.defaultModel;
|
||||
}
|
||||
|
||||
function openLink(url) {
|
||||
// Validate URL to prevent shell injection
|
||||
if (!url || !/^https?:\/\//i.test(url)) {
|
||||
console.warn("[openLink] Blocked non-HTTP URL: " + url);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var cp = require("child_process");
|
||||
var os = require("os");
|
||||
var safeUrl = url.replace(/["\\`$!]/g, ""); // strip dangerous chars
|
||||
if (os.platform() === "win32") {
|
||||
cp.exec('start "" "' + safeUrl + '"');
|
||||
} else {
|
||||
cp.exec('open "' + safeUrl + '"');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Could not open URL: " + url);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Login ----
|
||||
function login() {
|
||||
var apiKey = document.getElementById("apiKeyInput").value.trim();
|
||||
if (!apiKey) {
|
||||
showLoginError("Please enter an API key.");
|
||||
return;
|
||||
}
|
||||
|
||||
var model = document.getElementById("modelSelect").value;
|
||||
var btn = document.getElementById("loginBtn");
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Connecting...";
|
||||
hideLoginError();
|
||||
|
||||
validateApiKey(state.provider, apiKey, model, function (valid, error) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Connect & Start Chatting";
|
||||
|
||||
if (!valid) {
|
||||
showLoginError("Connection failed: " + (error || "Unknown error"));
|
||||
return;
|
||||
}
|
||||
|
||||
state.apiKey = apiKey;
|
||||
state.model = model;
|
||||
|
||||
// Persist non-sensitive preferences only. Keep the API key in memory for
|
||||
// this panel session so it is not exposed through browser storage.
|
||||
try {
|
||||
localStorage.setItem("ai_chat_provider", state.provider);
|
||||
localStorage.setItem("ai_chat_model", model);
|
||||
} catch (e) {}
|
||||
|
||||
showChatScreen();
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
state.apiKey = "";
|
||||
state.messages = [];
|
||||
state.projectContext = null;
|
||||
try {
|
||||
localStorage.removeItem("ai_chat_api_key");
|
||||
} catch (e) {}
|
||||
showLoginScreen();
|
||||
}
|
||||
|
||||
function showLoginError(msg) {
|
||||
var el = document.getElementById("loginError");
|
||||
el.textContent = msg;
|
||||
el.style.display = "block";
|
||||
}
|
||||
|
||||
function hideLoginError() {
|
||||
document.getElementById("loginError").style.display = "none";
|
||||
}
|
||||
|
||||
function toggleKeyVisibility() {
|
||||
var input = document.getElementById("apiKeyInput");
|
||||
var icon = document.getElementById("eyeIcon");
|
||||
if (input.type === "password") {
|
||||
input.type = "text";
|
||||
icon.textContent = "🙈";
|
||||
} else {
|
||||
input.type = "password";
|
||||
icon.textContent = "👁";
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Screen Navigation ----
|
||||
function showLoginScreen() {
|
||||
document.getElementById("loginScreen").style.display = "flex";
|
||||
document.getElementById("chatScreen").style.display = "none";
|
||||
}
|
||||
|
||||
function showChatScreen() {
|
||||
document.getElementById("loginScreen").style.display = "none";
|
||||
document.getElementById("chatScreen").style.display = "flex";
|
||||
|
||||
var config = PROVIDERS[state.provider];
|
||||
document.getElementById("headerTitle").textContent = config.name;
|
||||
document.getElementById("headerModel").textContent = state.model;
|
||||
|
||||
// Refresh project context
|
||||
refreshContext();
|
||||
}
|
||||
|
||||
// ---- Chat ----
|
||||
function sendMessage() {
|
||||
var input = document.getElementById("chatInput");
|
||||
var text = input.value.trim();
|
||||
if (!text || state.isStreaming) return;
|
||||
|
||||
input.value = "";
|
||||
autoResizeInput();
|
||||
|
||||
// Remove welcome message
|
||||
var welcome = document.querySelector(".welcome-msg");
|
||||
if (welcome) welcome.remove();
|
||||
|
||||
addMessage("user", text);
|
||||
state.messages.push({ role: "user", content: text });
|
||||
|
||||
// Trim history to prevent token overflow
|
||||
trimHistory();
|
||||
|
||||
sendToAI();
|
||||
}
|
||||
|
||||
function sendSuggestion(text) {
|
||||
document.getElementById("chatInput").value = text;
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
function trimHistory() {
|
||||
// Keep only the last MAX_HISTORY messages to avoid token overflow
|
||||
if (state.messages.length > MAX_HISTORY) {
|
||||
state.messages = state.messages.slice(state.messages.length - MAX_HISTORY);
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
state.messages = [];
|
||||
state.scriptQueue = [];
|
||||
state.scriptRunning = false;
|
||||
var container = document.getElementById("messages");
|
||||
container.innerHTML = "";
|
||||
// Re-add welcome message
|
||||
var welcome = document.createElement("div");
|
||||
welcome.className = "welcome-msg";
|
||||
welcome.innerHTML =
|
||||
'<p><strong>Welcome!</strong> I can help you edit in Premiere Pro. Try:</p>' +
|
||||
'<div class="suggestions">' +
|
||||
'<button class="suggestion" onclick="sendSuggestion(\'What clips are in my timeline?\')">What clips are in my timeline?</button>' +
|
||||
'<button class="suggestion" onclick="sendSuggestion(\'Add a cross dissolve to all cuts\')">Add a cross dissolve to all cuts</button>' +
|
||||
'<button class="suggestion" onclick="sendSuggestion(\'Export the active sequence as H.264\')">Export as H.264</button>' +
|
||||
'</div>';
|
||||
container.appendChild(welcome);
|
||||
document.getElementById("tokenCount").textContent = "";
|
||||
updateStatus("Ready");
|
||||
}
|
||||
|
||||
function sendToAI() {
|
||||
state.isStreaming = true;
|
||||
updateStatus("Thinking...");
|
||||
document.getElementById("sendBtn").disabled = true;
|
||||
showTypingIndicator();
|
||||
|
||||
// Build context-enriched messages
|
||||
var contextMsg = "";
|
||||
if (state.projectContext) {
|
||||
var ctx = state.projectContext;
|
||||
contextMsg = "[Current Premiere Pro context: ";
|
||||
if (ctx.hasProject) {
|
||||
contextMsg += "Project: " + ctx.name;
|
||||
if (ctx.activeSequence) {
|
||||
contextMsg += ", Active Sequence: " + ctx.activeSequence.name +
|
||||
" (" + ctx.activeSequence.frameSizeH + "x" + ctx.activeSequence.frameSizeV +
|
||||
", " + ctx.activeSequence.videoTracks + "V/" + ctx.activeSequence.audioTracks + "A tracks)";
|
||||
}
|
||||
contextMsg += ", " + ctx.numItems + " project items, " + ctx.numSequences + " sequences";
|
||||
} else {
|
||||
contextMsg += "No project open";
|
||||
}
|
||||
contextMsg += "]";
|
||||
}
|
||||
|
||||
// Prepend context to the first user message if available
|
||||
var messagesForAPI = state.messages.slice();
|
||||
if (contextMsg && messagesForAPI.length > 0) {
|
||||
var lastUserIdx = -1;
|
||||
for (var i = messagesForAPI.length - 1; i >= 0; i--) {
|
||||
if (messagesForAPI[i].role === "user") { lastUserIdx = i; break; }
|
||||
}
|
||||
if (lastUserIdx >= 0) {
|
||||
messagesForAPI[lastUserIdx] = {
|
||||
role: "user",
|
||||
content: contextMsg + "\n\n" + messagesForAPI[lastUserIdx].content,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
callAI(
|
||||
state.provider,
|
||||
state.apiKey,
|
||||
state.model,
|
||||
messagesForAPI,
|
||||
state.customSystemPrompt,
|
||||
{ temperature: state.temperature, maxTokens: state.maxTokens },
|
||||
function (err, result) {
|
||||
hideTypingIndicator();
|
||||
state.isStreaming = false;
|
||||
document.getElementById("sendBtn").disabled = false;
|
||||
|
||||
if (err) {
|
||||
addMessage("assistant", "**Error:** " + err);
|
||||
updateStatus("Error");
|
||||
return;
|
||||
}
|
||||
|
||||
var text = result.text || "(empty response)";
|
||||
state.messages.push({ role: "assistant", content: text });
|
||||
addMessage("assistant", text);
|
||||
|
||||
// Update token count
|
||||
var usage = result.usage || {};
|
||||
var tokenInfo = "";
|
||||
if (usage.input_tokens) tokenInfo = usage.input_tokens + " in / " + usage.output_tokens + " out";
|
||||
else if (usage.promptTokenCount) tokenInfo = usage.promptTokenCount + " in / " + usage.candidatesTokenCount + " out";
|
||||
document.getElementById("tokenCount").textContent = tokenInfo;
|
||||
|
||||
updateStatus("Ready");
|
||||
|
||||
// Check for ExtendScript code blocks and auto-execute
|
||||
extractAndExecuteScripts(text);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleInputKey(e) {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function autoResizeInput() {
|
||||
var ta = document.getElementById("chatInput");
|
||||
ta.style.height = "auto";
|
||||
ta.style.height = Math.min(ta.scrollHeight, 120) + "px";
|
||||
}
|
||||
|
||||
// ---- Message Rendering ----
|
||||
function addMessage(role, content) {
|
||||
var container = document.getElementById("messages");
|
||||
var msgDiv = document.createElement("div");
|
||||
msgDiv.className = "msg " + role;
|
||||
|
||||
var bubble = document.createElement("div");
|
||||
bubble.className = "msg-bubble";
|
||||
bubble.innerHTML = renderMarkdown(content);
|
||||
|
||||
var meta = document.createElement("div");
|
||||
meta.className = "msg-meta";
|
||||
meta.textContent = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
|
||||
msgDiv.appendChild(bubble);
|
||||
msgDiv.appendChild(meta);
|
||||
container.appendChild(msgDiv);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
// Extract code blocks first to protect them from escaping
|
||||
var codeBlocks = [];
|
||||
var placeholder = "\x00CODE_BLOCK_";
|
||||
var processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, function (match, lang, code) {
|
||||
var idx = codeBlocks.length;
|
||||
codeBlocks.push({ lang: lang, code: code.trim() });
|
||||
return placeholder + idx + "\x00";
|
||||
});
|
||||
|
||||
// Extract inline code
|
||||
var inlineCodes = [];
|
||||
var inlinePlaceholder = "\x00INLINE_CODE_";
|
||||
processed = processed.replace(/`([^`]+)`/g, function (match, code) {
|
||||
var idx = inlineCodes.length;
|
||||
inlineCodes.push(code);
|
||||
return inlinePlaceholder + idx + "\x00";
|
||||
});
|
||||
|
||||
// Now escape HTML on the remaining text
|
||||
var html = escapeHtml(processed);
|
||||
|
||||
// Bold
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||
|
||||
// Italic
|
||||
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||
|
||||
// Line breaks
|
||||
html = html.replace(/\n/g, "<br/>");
|
||||
|
||||
// Restore inline code (escaped content)
|
||||
for (var i = 0; i < inlineCodes.length; i++) {
|
||||
html = html.replace(inlinePlaceholder + i + "\x00",
|
||||
"<code>" + escapeHtml(inlineCodes[i]) + "</code>");
|
||||
}
|
||||
|
||||
// Restore code blocks (escaped content)
|
||||
for (var j = 0; j < codeBlocks.length; j++) {
|
||||
var cls = codeBlocks[j].lang ? ' class="lang-' + escapeHtml(codeBlocks[j].lang) + '"' : "";
|
||||
html = html.replace(placeholder + j + "\x00",
|
||||
'<pre><code' + cls + '>' + escapeHtml(codeBlocks[j].code) + '</code></pre>');
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
var div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showTypingIndicator() {
|
||||
var container = document.getElementById("messages");
|
||||
var typing = document.createElement("div");
|
||||
typing.className = "msg assistant";
|
||||
typing.id = "typingIndicator";
|
||||
typing.innerHTML = '<div class="typing"><span></span><span></span><span></span></div>';
|
||||
container.appendChild(typing);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function hideTypingIndicator() {
|
||||
var el = document.getElementById("typingIndicator");
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
function updateStatus(text) {
|
||||
document.getElementById("statusText").textContent = text;
|
||||
}
|
||||
|
||||
// ---- ExtendScript Execution ----
|
||||
function extractAndExecuteScripts(text) {
|
||||
// Find ```extendscript ... ``` code blocks
|
||||
var regex = /```(?:extendscript|jsx|javascript)\n([\s\S]*?)```/g;
|
||||
var match;
|
||||
var scripts = [];
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
scripts.push(match[1].trim());
|
||||
}
|
||||
|
||||
if (scripts.length === 0) return;
|
||||
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
if (state.autoExec) {
|
||||
// Queue scripts for sequential execution to avoid race conditions
|
||||
state.scriptQueue.push(scripts[i]);
|
||||
} else {
|
||||
showScriptPreview(scripts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.autoExec && !state.scriptRunning) {
|
||||
runNextScript();
|
||||
}
|
||||
}
|
||||
|
||||
function runNextScript() {
|
||||
if (state.scriptQueue.length === 0) {
|
||||
state.scriptRunning = false;
|
||||
return;
|
||||
}
|
||||
state.scriptRunning = true;
|
||||
var script = state.scriptQueue.shift();
|
||||
executeExtendScript(script, function () {
|
||||
runNextScript();
|
||||
});
|
||||
}
|
||||
|
||||
function executeExtendScript(script, onComplete) {
|
||||
// Wrap in try/catch with helpers
|
||||
var wrappedScript =
|
||||
"(function() {\n" +
|
||||
" try {\n" +
|
||||
script + "\n" +
|
||||
" } catch(e) {\n" +
|
||||
" return __error(e.toString());\n" +
|
||||
" }\n" +
|
||||
"})();";
|
||||
|
||||
updateStatus("Executing script...");
|
||||
|
||||
cs.evalScript(wrappedScript, function (result) {
|
||||
updateStatus("Ready");
|
||||
|
||||
var resultDiv = document.createElement("div");
|
||||
resultDiv.className = "msg assistant";
|
||||
|
||||
var block = document.createElement("div");
|
||||
block.className = "msg-bubble";
|
||||
|
||||
var scriptBlock = document.createElement("div");
|
||||
scriptBlock.className = "script-block";
|
||||
|
||||
var header = document.createElement("div");
|
||||
header.className = "script-header";
|
||||
header.innerHTML = '<span class="label">ExtendScript Result</span>';
|
||||
|
||||
var resultContent = document.createElement("div");
|
||||
|
||||
try {
|
||||
if (result && result !== "undefined" && result !== "null") {
|
||||
var parsed = JSON.parse(result);
|
||||
if (parsed.success) {
|
||||
resultContent.className = "script-result success";
|
||||
resultContent.textContent = JSON.stringify(parsed.data, null, 2);
|
||||
|
||||
// Feed result back to AI as context
|
||||
var resultMsg = "[ExtendScript executed successfully. Result: " + JSON.stringify(parsed.data) + "]";
|
||||
state.messages.push({ role: "assistant", content: resultMsg });
|
||||
} else {
|
||||
resultContent.className = "script-result error";
|
||||
resultContent.textContent = "Error: " + (parsed.error || "Unknown error");
|
||||
|
||||
var errMsg = "[ExtendScript execution error: " + (parsed.error || "Unknown error") + "]";
|
||||
state.messages.push({ role: "assistant", content: errMsg });
|
||||
}
|
||||
} else {
|
||||
resultContent.className = "script-result success";
|
||||
resultContent.textContent = "(no return value)";
|
||||
}
|
||||
} catch (e) {
|
||||
resultContent.className = "script-result error";
|
||||
resultContent.textContent = "Parse error: " + result;
|
||||
}
|
||||
|
||||
scriptBlock.appendChild(header);
|
||||
scriptBlock.appendChild(resultContent);
|
||||
block.appendChild(scriptBlock);
|
||||
resultDiv.appendChild(block);
|
||||
|
||||
var container = document.getElementById("messages");
|
||||
container.appendChild(resultDiv);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
// Refresh context after executing scripts
|
||||
refreshContext();
|
||||
|
||||
// Signal completion for sequential queue
|
||||
if (typeof onComplete === "function") onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
function showScriptPreview(script) {
|
||||
var container = document.getElementById("messages");
|
||||
var msgDiv = document.createElement("div");
|
||||
msgDiv.className = "msg assistant";
|
||||
|
||||
var block = document.createElement("div");
|
||||
block.className = "msg-bubble";
|
||||
|
||||
var scriptBlock = document.createElement("div");
|
||||
scriptBlock.className = "script-block";
|
||||
|
||||
var header = document.createElement("div");
|
||||
header.className = "script-header";
|
||||
header.innerHTML = '<span class="label">ExtendScript (preview)</span>';
|
||||
|
||||
var execBtn = document.createElement("button");
|
||||
execBtn.className = "exec-btn";
|
||||
execBtn.textContent = "Execute";
|
||||
execBtn.onclick = function () {
|
||||
execBtn.disabled = true;
|
||||
execBtn.textContent = "Running...";
|
||||
executeExtendScript(script);
|
||||
};
|
||||
header.appendChild(execBtn);
|
||||
|
||||
var code = document.createElement("pre");
|
||||
code.innerHTML = "<code>" + escapeHtml(script) + "</code>";
|
||||
|
||||
scriptBlock.appendChild(header);
|
||||
scriptBlock.appendChild(code);
|
||||
block.appendChild(scriptBlock);
|
||||
msgDiv.appendChild(block);
|
||||
container.appendChild(msgDiv);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// ---- Project Context ----
|
||||
function refreshContext() {
|
||||
cs.evalScript("getProjectContext()", function (result) {
|
||||
try {
|
||||
var parsed = JSON.parse(result);
|
||||
if (parsed.success && parsed.data) {
|
||||
state.projectContext = parsed.data;
|
||||
var banner = document.getElementById("contextBanner");
|
||||
var text = document.getElementById("contextText");
|
||||
banner.style.display = "flex";
|
||||
|
||||
if (parsed.data.hasProject) {
|
||||
var info = parsed.data.name;
|
||||
if (parsed.data.activeSequence) {
|
||||
info += " → " + parsed.data.activeSequence.name;
|
||||
}
|
||||
text.textContent = info;
|
||||
} else {
|
||||
text.textContent = "No project open";
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Not in CEP environment
|
||||
var banner = document.getElementById("contextBanner");
|
||||
banner.style.display = "flex";
|
||||
document.getElementById("contextText").textContent = "Not connected to Premiere Pro";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Settings ----
|
||||
function openSettings() {
|
||||
var modal = document.getElementById("settingsModal");
|
||||
modal.style.display = "flex";
|
||||
|
||||
// Populate settings
|
||||
var config = PROVIDERS[state.provider];
|
||||
var select = document.getElementById("settingsModel");
|
||||
select.innerHTML = "";
|
||||
for (var i = 0; i < config.models.length; i++) {
|
||||
var opt = document.createElement("option");
|
||||
opt.value = config.models[i].id;
|
||||
opt.textContent = config.models[i].label;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
select.value = state.model;
|
||||
|
||||
document.getElementById("settingsTemp").value = state.temperature;
|
||||
document.getElementById("settingsTempVal").textContent = state.temperature;
|
||||
document.getElementById("settingsMaxTokens").value = state.maxTokens;
|
||||
document.getElementById("settingsSystemPrompt").value = state.customSystemPrompt;
|
||||
document.getElementById("settingsAutoExec").checked = state.autoExec;
|
||||
|
||||
// Bind temp slider
|
||||
document.getElementById("settingsTemp").oninput = function () {
|
||||
document.getElementById("settingsTempVal").textContent = this.value;
|
||||
};
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
document.getElementById("settingsModal").style.display = "none";
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
state.model = document.getElementById("settingsModel").value;
|
||||
state.temperature = parseFloat(document.getElementById("settingsTemp").value);
|
||||
state.maxTokens = parseInt(document.getElementById("settingsMaxTokens").value, 10);
|
||||
state.customSystemPrompt = document.getElementById("settingsSystemPrompt").value;
|
||||
state.autoExec = document.getElementById("settingsAutoExec").checked;
|
||||
|
||||
document.getElementById("headerModel").textContent = state.model;
|
||||
|
||||
// Persist
|
||||
try {
|
||||
localStorage.setItem("ai_chat_model", state.model);
|
||||
localStorage.setItem("ai_chat_temperature", String(state.temperature));
|
||||
localStorage.setItem("ai_chat_max_tokens", String(state.maxTokens));
|
||||
localStorage.setItem("ai_chat_system_prompt", state.customSystemPrompt);
|
||||
localStorage.setItem("ai_chat_auto_exec", String(state.autoExec));
|
||||
} catch (e) {}
|
||||
|
||||
closeSettings();
|
||||
}
|
||||
|
||||
function changeApiKey() {
|
||||
closeSettings();
|
||||
logout();
|
||||
}
|
||||
|
||||
// ---- Init ----
|
||||
(function init() {
|
||||
updateProviderUI();
|
||||
|
||||
// Restore saved settings
|
||||
try {
|
||||
// Remove keys persisted by older releases.
|
||||
localStorage.removeItem("ai_chat_api_key");
|
||||
var savedProvider = localStorage.getItem("ai_chat_provider");
|
||||
var savedModel = localStorage.getItem("ai_chat_model");
|
||||
var savedTemp = localStorage.getItem("ai_chat_temperature");
|
||||
var savedMaxTokens = localStorage.getItem("ai_chat_max_tokens");
|
||||
var savedSystemPrompt = localStorage.getItem("ai_chat_system_prompt");
|
||||
var savedAutoExec = localStorage.getItem("ai_chat_auto_exec");
|
||||
|
||||
if (savedProvider) {
|
||||
state.provider = savedProvider;
|
||||
selectProvider(savedProvider);
|
||||
}
|
||||
if (savedTemp) state.temperature = parseFloat(savedTemp);
|
||||
if (savedMaxTokens) state.maxTokens = parseInt(savedMaxTokens, 10);
|
||||
if (savedSystemPrompt) state.customSystemPrompt = savedSystemPrompt;
|
||||
if (savedAutoExec !== null) state.autoExec = savedAutoExec === "true";
|
||||
|
||||
if (savedModel) document.getElementById("modelSelect").value = savedModel;
|
||||
} catch (e) {}
|
||||
|
||||
showLoginScreen();
|
||||
})();
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
/* ===== Reset & Base ===== */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg-primary: #1e1e2e;
|
||||
--bg-secondary: #252536;
|
||||
--bg-tertiary: #2d2d44;
|
||||
--bg-input: #1a1a2a;
|
||||
--bg-hover: #353550;
|
||||
--text-primary: #e0e0f0;
|
||||
--text-secondary: #9090b0;
|
||||
--text-muted: #606080;
|
||||
--accent: #7C3AED;
|
||||
--accent-hover: #6D28D9;
|
||||
--accent-light: rgba(124, 58, 237, 0.15);
|
||||
--success: #22C55E;
|
||||
--error: #EF4444;
|
||||
--warning: #F59E0B;
|
||||
--border: #3a3a52;
|
||||
--border-light: #44446a;
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: "SF Mono", "Fira Code", "JetBrains Mono", monospace;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%; height: 100%;
|
||||
font-family: var(--font);
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* ===== Screens ===== */
|
||||
.screen { width: 100%; height: 100%; }
|
||||
|
||||
/* ===== Login Screen ===== */
|
||||
.login-container {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; height: 100%; padding: 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
.logo { margin-bottom: 4px; }
|
||||
.login-container h1 {
|
||||
font-size: 20px; font-weight: 700; color: var(--text-primary);
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 12px; color: var(--text-secondary); text-align: center;
|
||||
max-width: 280px; line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Provider Tabs */
|
||||
.provider-tabs {
|
||||
display: flex; gap: 8px; width: 100%; max-width: 320px;
|
||||
}
|
||||
.tab {
|
||||
flex: 1; padding: 10px 16px; border: 1px solid var(--border);
|
||||
background: var(--bg-secondary); color: var(--text-secondary);
|
||||
border-radius: var(--radius); cursor: pointer;
|
||||
font-size: 13px; font-weight: 600; transition: all 0.15s;
|
||||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||
}
|
||||
.tab:hover { border-color: var(--border-light); color: var(--text-primary); }
|
||||
.tab.active {
|
||||
border-color: var(--accent); background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
}
|
||||
.tab-icon { font-size: 14px; }
|
||||
|
||||
/* Form */
|
||||
.form-group {
|
||||
width: 100%; max-width: 320px; display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.form-group label {
|
||||
font-size: 12px; font-weight: 600; color: var(--text-secondary);
|
||||
}
|
||||
.input-row { display: flex; gap: 6px; }
|
||||
.input-row input, .input-row textarea { flex: 1; }
|
||||
|
||||
input[type="text"], input[type="password"], input[type="number"],
|
||||
select, textarea {
|
||||
padding: 10px 12px; background: var(--bg-input);
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
color: var(--text-primary); font-size: 13px; font-family: var(--font);
|
||||
outline: none; transition: border-color 0.15s; width: 100%;
|
||||
}
|
||||
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
||||
select { cursor: pointer; }
|
||||
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none; appearance: none; width: 100%; height: 4px;
|
||||
background: var(--bg-tertiary); border-radius: 2px; outline: none;
|
||||
}
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; width: 16px; height: 16px;
|
||||
background: var(--accent); border-radius: 50%; cursor: pointer;
|
||||
}
|
||||
|
||||
.hint { font-size: 11px; color: var(--text-muted); }
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
width: 100%; max-width: 320px; padding: 12px 20px;
|
||||
background: var(--accent); color: white; border: none;
|
||||
border-radius: var(--radius); font-size: 14px; font-weight: 600;
|
||||
cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.btn-secondary {
|
||||
padding: 8px 16px; background: var(--bg-tertiary);
|
||||
color: var(--text-primary); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); font-size: 12px; cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-secondary:hover { background: var(--bg-hover); }
|
||||
|
||||
.icon-btn {
|
||||
background: none; border: none; color: var(--text-secondary);
|
||||
cursor: pointer; font-size: 16px; padding: 4px;
|
||||
border-radius: 4px; transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.icon-btn:hover { color: var(--text-primary); background: var(--bg-hover); }
|
||||
.icon-btn.small { font-size: 14px; }
|
||||
.icon-btn.tiny { font-size: 12px; padding: 2px; }
|
||||
|
||||
.error-msg {
|
||||
width: 100%; max-width: 320px; padding: 10px 12px;
|
||||
background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3);
|
||||
border-radius: var(--radius); color: var(--error); font-size: 12px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.login-footer p {
|
||||
font-size: 11px; color: var(--text-muted); text-align: center;
|
||||
}
|
||||
|
||||
/* ===== Chat Screen ===== */
|
||||
#chatScreen {
|
||||
display: flex; flex-direction: column; height: 100%;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.chat-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 14px; background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border); flex-shrink: 0;
|
||||
}
|
||||
.header-left { display: flex; align-items: center; gap: 8px; }
|
||||
.header-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
}
|
||||
.header-dot.connected { background: var(--success); }
|
||||
.header-title { font-weight: 700; font-size: 14px; }
|
||||
.header-model { font-size: 11px; color: var(--text-muted); }
|
||||
.header-right { display: flex; gap: 4px; }
|
||||
|
||||
/* Context Banner */
|
||||
.context-banner {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 6px 14px; background: var(--accent-light);
|
||||
border-bottom: 1px solid var(--border); font-size: 12px;
|
||||
color: var(--text-secondary); flex-shrink: 0;
|
||||
}
|
||||
.context-icon { font-size: 14px; }
|
||||
|
||||
/* Messages */
|
||||
.messages {
|
||||
flex: 1; overflow-y: auto; padding: 16px;
|
||||
display: flex; flex-direction: column; gap: 12px;
|
||||
}
|
||||
.messages::-webkit-scrollbar { width: 6px; }
|
||||
.messages::-webkit-scrollbar-track { background: transparent; }
|
||||
.messages::-webkit-scrollbar-thumb {
|
||||
background: var(--border); border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Welcome */
|
||||
.welcome-msg {
|
||||
text-align: center; padding: 24px 0;
|
||||
}
|
||||
.welcome-msg p { color: var(--text-secondary); margin-bottom: 16px; font-size: 13px; }
|
||||
.suggestions { display: flex; flex-direction: column; gap: 8px; }
|
||||
.suggestion {
|
||||
padding: 10px 14px; background: var(--bg-secondary);
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
color: var(--text-primary); font-size: 12px; cursor: pointer;
|
||||
text-align: left; transition: all 0.15s;
|
||||
}
|
||||
.suggestion:hover { border-color: var(--accent); background: var(--accent-light); }
|
||||
|
||||
/* Message Bubbles */
|
||||
.msg {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
max-width: 92%; animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; } }
|
||||
|
||||
.msg.user { align-self: flex-end; }
|
||||
.msg.assistant { align-self: flex-start; }
|
||||
|
||||
.msg-bubble {
|
||||
padding: 10px 14px; border-radius: var(--radius-lg);
|
||||
font-size: 13px; line-height: 1.55; word-wrap: break-word; overflow-wrap: break-word;
|
||||
}
|
||||
.msg.user .msg-bubble {
|
||||
background: var(--accent); color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.msg.assistant .msg-bubble {
|
||||
background: var(--bg-secondary); color: var(--text-primary);
|
||||
border: 1px solid var(--border); border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.msg-meta {
|
||||
font-size: 10px; color: var(--text-muted); padding: 0 4px;
|
||||
}
|
||||
.msg.user .msg-meta { text-align: right; }
|
||||
|
||||
/* Code blocks inside messages */
|
||||
.msg-bubble pre {
|
||||
background: var(--bg-primary); border: 1px solid var(--border);
|
||||
border-radius: 6px; padding: 10px 12px; margin: 8px 0 4px;
|
||||
overflow-x: auto; font-family: var(--font-mono); font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.msg-bubble code {
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
background: rgba(124,58,237,0.15); padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.msg-bubble pre code { background: none; padding: 0; }
|
||||
|
||||
/* Script execution block */
|
||||
.script-block {
|
||||
margin: 8px 0; padding: 8px 12px;
|
||||
background: var(--bg-primary); border: 1px solid var(--border);
|
||||
border-radius: 6px; font-size: 11px;
|
||||
}
|
||||
.script-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 6px; color: var(--text-muted);
|
||||
}
|
||||
.script-header .label { font-weight: 600; }
|
||||
.script-result {
|
||||
padding: 6px 10px; border-radius: 4px; margin-top: 6px;
|
||||
font-family: var(--font-mono); font-size: 11px; line-height: 1.4;
|
||||
}
|
||||
.script-result.success { background: rgba(34,197,94,0.1); color: var(--success); }
|
||||
.script-result.error { background: rgba(239,68,68,0.1); color: var(--error); }
|
||||
|
||||
.exec-btn {
|
||||
padding: 4px 10px; background: var(--accent); color: white;
|
||||
border: none; border-radius: 4px; font-size: 11px; cursor: pointer;
|
||||
}
|
||||
.exec-btn:hover { background: var(--accent-hover); }
|
||||
|
||||
/* Typing indicator */
|
||||
.typing {
|
||||
display: flex; gap: 4px; padding: 12px 14px;
|
||||
background: var(--bg-secondary); border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg); border-bottom-left-radius: 4px;
|
||||
width: fit-content;
|
||||
}
|
||||
.typing span {
|
||||
width: 7px; height: 7px; background: var(--text-muted);
|
||||
border-radius: 50%; animation: bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
.typing span:nth-child(1) { animation-delay: 0s; }
|
||||
.typing span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Input Area */
|
||||
.input-area {
|
||||
padding: 12px 14px; border-top: 1px solid var(--border);
|
||||
background: var(--bg-secondary); flex-shrink: 0;
|
||||
}
|
||||
.input-area .input-row { display: flex; gap: 8px; align-items: flex-end; }
|
||||
.input-area textarea {
|
||||
flex: 1; padding: 10px 12px; background: var(--bg-input);
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
color: var(--text-primary); font-size: 13px; font-family: var(--font);
|
||||
outline: none; resize: none; max-height: 120px; min-height: 38px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.input-area textarea:focus { border-color: var(--accent); }
|
||||
|
||||
.send-btn {
|
||||
width: 38px; height: 38px; background: var(--accent);
|
||||
color: white; border: none; border-radius: var(--radius);
|
||||
cursor: pointer; display: flex; align-items: center;
|
||||
justify-content: center; flex-shrink: 0; transition: background 0.15s;
|
||||
}
|
||||
.send-btn:hover { background: var(--accent-hover); }
|
||||
.send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.input-footer {
|
||||
display: flex; justify-content: space-between;
|
||||
padding: 6px 4px 0; font-size: 10px; color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ===== Settings Modal ===== */
|
||||
.modal {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
z-index: 100; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-backdrop {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.6);
|
||||
}
|
||||
.modal-content {
|
||||
position: relative; background: var(--bg-secondary);
|
||||
border: 1px solid var(--border); border-radius: var(--radius-lg);
|
||||
padding: 20px; width: 90%; max-width: 380px; max-height: 80%;
|
||||
overflow-y: auto; box-shadow: var(--shadow);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.modal-header h2 { font-size: 16px; font-weight: 700; }
|
||||
.modal-body { display: flex; flex-direction: column; gap: 14px; }
|
||||
.modal-body .form-group { max-width: none; }
|
||||
.modal-body .btn-primary { max-width: none; }
|
||||
|
||||
/* Checkbox label */
|
||||
.checkbox-label {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: 13px; color: var(--text-primary); cursor: pointer;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
width: 16px; height: 16px; accent-color: var(--accent);
|
||||
}
|
||||
Reference in New Issue
Block a user