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:
João Henrique
2026-09-08 09:59:31 -04:00
commit b541f502ba
1507 changed files with 387650 additions and 0 deletions
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+661
View File
@@ -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
View File
@@ -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);
}