Feature:Anthropic AI Integration
This commit is contained in:
parent
6bbca03719
commit
17b10b1ad0
@ -29,6 +29,7 @@
|
|||||||
},
|
},
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.56.0",
|
||||||
"@google/genai": "^1.8.0",
|
"@google/genai": "^1.8.0",
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
|
import { html, css, LitElement } from "../assets/lit-core-2.7.4.min.js"
|
||||||
|
|
||||||
export class ApiKeyHeader extends LitElement {
|
export class ApiKeyHeader extends LitElement {
|
||||||
static properties = {
|
static properties = {
|
||||||
apiKey: { type: String },
|
apiKey: { type: String },
|
||||||
isLoading: { type: Boolean },
|
isLoading: { type: Boolean },
|
||||||
errorMessage: { type: String },
|
errorMessage: { type: String },
|
||||||
selectedProvider: { type: String },
|
selectedProvider: { type: String },
|
||||||
};
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
@ -272,304 +272,336 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
:host-context(body.has-glass) .close-button:hover {
|
:host-context(body.has-glass) .close-button:hover {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super()
|
||||||
this.dragState = null;
|
this.dragState = null
|
||||||
this.wasJustDragged = false;
|
this.wasJustDragged = false
|
||||||
this.apiKey = '';
|
this.apiKey = ""
|
||||||
this.isLoading = false;
|
this.isLoading = false
|
||||||
this.errorMessage = '';
|
this.errorMessage = ""
|
||||||
this.validatedApiKey = null;
|
this.validatedApiKey = null
|
||||||
this.selectedProvider = 'openai';
|
this.selectedProvider = "openai"
|
||||||
|
|
||||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
this.handleMouseMove = this.handleMouseMove.bind(this)
|
||||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
this.handleMouseUp = this.handleMouseUp.bind(this)
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this)
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this)
|
||||||
this.handleInput = this.handleInput.bind(this);
|
this.handleInput = this.handleInput.bind(this)
|
||||||
this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
|
this.handleAnimationEnd = this.handleAnimationEnd.bind(this)
|
||||||
this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this);
|
this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this)
|
||||||
this.handleProviderChange = this.handleProviderChange.bind(this);
|
this.handleProviderChange = this.handleProviderChange.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.apiKey = ""
|
||||||
|
this.isLoading = false
|
||||||
|
this.errorMessage = ""
|
||||||
|
this.validatedApiKey = null
|
||||||
|
this.selectedProvider = "openai"
|
||||||
|
this.requestUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleMouseDown(e) {
|
||||||
|
if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON" || e.target.tagName === "SELECT") {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
e.preventDefault()
|
||||||
this.apiKey = '';
|
|
||||||
this.isLoading = false;
|
const { ipcRenderer } = window.require("electron")
|
||||||
this.errorMessage = '';
|
const initialPosition = await ipcRenderer.invoke("get-header-position")
|
||||||
this.validatedApiKey = null;
|
|
||||||
this.selectedProvider = 'openai';
|
this.dragState = {
|
||||||
this.requestUpdate();
|
initialMouseX: e.screenX,
|
||||||
|
initialMouseY: e.screenY,
|
||||||
|
initialWindowX: initialPosition.x,
|
||||||
|
initialWindowY: initialPosition.y,
|
||||||
|
moved: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleMouseDown(e) {
|
window.addEventListener("mousemove", this.handleMouseMove)
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') {
|
window.addEventListener("mouseup", this.handleMouseUp, { once: true })
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
handleMouseMove(e) {
|
||||||
|
if (!this.dragState) return
|
||||||
|
|
||||||
|
const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX)
|
||||||
|
const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY)
|
||||||
|
|
||||||
|
if (deltaX > 3 || deltaY > 3) {
|
||||||
|
this.dragState.moved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX)
|
||||||
|
const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY)
|
||||||
|
|
||||||
|
const { ipcRenderer } = window.require("electron")
|
||||||
|
ipcRenderer.invoke("move-header-to", newWindowX, newWindowY)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp(e) {
|
||||||
|
if (!this.dragState) return
|
||||||
|
|
||||||
|
const wasDragged = this.dragState.moved
|
||||||
|
|
||||||
|
window.removeEventListener("mousemove", this.handleMouseMove)
|
||||||
|
this.dragState = null
|
||||||
|
|
||||||
|
if (wasDragged) {
|
||||||
|
this.wasJustDragged = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.wasJustDragged = false
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInput(e) {
|
||||||
|
this.apiKey = e.target.value
|
||||||
|
this.errorMessage = ""
|
||||||
|
console.log("Input changed:", this.apiKey?.length || 0, "chars")
|
||||||
|
|
||||||
|
this.requestUpdate()
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
const inputField = this.shadowRoot?.querySelector(".apikey-input")
|
||||||
|
if (inputField && this.isInputFocused) {
|
||||||
|
inputField.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleProviderChange(e) {
|
||||||
|
this.selectedProvider = e.target.value
|
||||||
|
this.errorMessage = ""
|
||||||
|
console.log("Provider changed to:", this.selectedProvider)
|
||||||
|
this.requestUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePaste(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.errorMessage = ""
|
||||||
|
const clipboardText = (e.clipboardData || window.clipboardData).getData("text")
|
||||||
|
console.log("Paste event detected:", clipboardText?.substring(0, 10) + "...")
|
||||||
|
|
||||||
|
if (clipboardText) {
|
||||||
|
this.apiKey = clipboardText.trim()
|
||||||
|
|
||||||
|
const inputElement = e.target
|
||||||
|
inputElement.value = this.apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestUpdate()
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
const inputField = this.shadowRoot?.querySelector(".apikey-input")
|
||||||
|
if (inputField) {
|
||||||
|
inputField.focus()
|
||||||
|
inputField.setSelectionRange(inputField.value.length, inputField.value.length)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyPress(e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault()
|
||||||
|
this.handleSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubmit() {
|
||||||
|
if (this.wasJustDragged || this.isLoading || !this.apiKey.trim()) {
|
||||||
|
console.log("Submit blocked:", {
|
||||||
|
wasJustDragged: this.wasJustDragged,
|
||||||
|
isLoading: this.isLoading,
|
||||||
|
hasApiKey: !!this.apiKey.trim(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Starting API key validation...")
|
||||||
|
this.isLoading = true
|
||||||
|
this.errorMessage = ""
|
||||||
|
this.requestUpdate()
|
||||||
|
|
||||||
|
const apiKey = this.apiKey.trim()
|
||||||
|
const isValid = false
|
||||||
|
try {
|
||||||
|
const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider)
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
console.log("API key valid - starting slide out animation")
|
||||||
|
this.startSlideOutAnimation()
|
||||||
|
this.validatedApiKey = this.apiKey.trim()
|
||||||
|
this.validatedProvider = this.selectedProvider
|
||||||
|
} else {
|
||||||
|
this.errorMessage = "Invalid API key - please check and try again"
|
||||||
|
console.log("API key validation failed")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API key validation error:", error)
|
||||||
|
this.errorMessage = "Validation error - please try again"
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
this.requestUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateApiKey(apiKey, provider = "openai") {
|
||||||
|
if (!apiKey || apiKey.length < 15) return false
|
||||||
|
|
||||||
|
if (provider === "openai") {
|
||||||
|
if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Validating OpenAI API key...")
|
||||||
|
|
||||||
|
const response = await fetch("https://api.openai.com/v1/models", {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const hasGPTModels = data.data && data.data.some((m) => m.id.startsWith("gpt-"))
|
||||||
|
if (hasGPTModels) {
|
||||||
|
console.log("OpenAI API key validation successful")
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
console.log("API key valid but no GPT models available")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
console.log("API key validation failed:", response.status, errorData.error?.message || "Unknown error")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API key validation network error:", error)
|
||||||
|
return apiKey.length >= 20 // Fallback for network issues
|
||||||
|
}
|
||||||
|
} else if (provider === "gemini") {
|
||||||
|
// Gemini API keys typically start with 'AIza'
|
||||||
|
if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Validating Gemini API key...")
|
||||||
|
|
||||||
|
// Test the API key with a simple models list request
|
||||||
|
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`)
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.models && data.models.length > 0) {
|
||||||
|
console.log("Gemini API key validation successful")
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
console.log("Gemini API key validation failed")
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gemini API key validation network error:", error)
|
||||||
|
return apiKey.length >= 20 // Fallback
|
||||||
|
}
|
||||||
|
} else if (provider === "anthropic") {
|
||||||
|
// Anthropic API keys typically start with 'sk-ant-'
|
||||||
|
if (!apiKey.startsWith("sk-ant-") || !apiKey.match(/^[A-Za-z0-9_-]+$/)) return false
|
||||||
|
|
||||||
const { ipcRenderer } = window.require('electron');
|
try {
|
||||||
const initialPosition = await ipcRenderer.invoke('get-header-position');
|
console.log("Validating Anthropic API key...")
|
||||||
|
|
||||||
this.dragState = {
|
// Test the API key with a simple request
|
||||||
initialMouseX: e.screenX,
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||||
initialMouseY: e.screenY,
|
method: "POST",
|
||||||
initialWindowX: initialPosition.x,
|
headers: {
|
||||||
initialWindowY: initialPosition.y,
|
"Content-Type": "application/json",
|
||||||
moved: false,
|
"x-api-key": apiKey,
|
||||||
};
|
"anthropic-version": "2023-06-01",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "claude-3-haiku-20240307",
|
||||||
|
max_tokens: 10,
|
||||||
|
messages: [{ role: "user", content: "Hi" }],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
window.addEventListener('mousemove', this.handleMouseMove);
|
if (response.ok || response.status === 400) {
|
||||||
window.addEventListener('mouseup', this.handleMouseUp, { once: true });
|
// 400 is also acceptable as it means the API key is valid but request format might be wrong
|
||||||
}
|
console.log("Anthropic API key validation successful")
|
||||||
|
return true
|
||||||
handleMouseMove(e) {
|
|
||||||
if (!this.dragState) return;
|
|
||||||
|
|
||||||
const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX);
|
|
||||||
const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY);
|
|
||||||
|
|
||||||
if (deltaX > 3 || deltaY > 3) {
|
|
||||||
this.dragState.moved = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX);
|
console.log("Anthropic API key validation failed:", response.status)
|
||||||
const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY);
|
return false
|
||||||
|
} catch (error) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
console.error("Anthropic API key validation network error:", error)
|
||||||
ipcRenderer.invoke('move-header-to', newWindowX, newWindowY);
|
return apiKey.length >= 20 // Fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp(e) {
|
return false
|
||||||
if (!this.dragState) return;
|
}
|
||||||
|
|
||||||
const wasDragged = this.dragState.moved;
|
startSlideOutAnimation() {
|
||||||
|
this.classList.add("sliding-out")
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
handleUsePicklesKey(e) {
|
||||||
this.dragState = null;
|
e.preventDefault()
|
||||||
|
if (this.wasJustDragged) return
|
||||||
|
|
||||||
if (wasDragged) {
|
console.log("Requesting Firebase authentication from main process...")
|
||||||
this.wasJustDragged = true;
|
if (window.require) {
|
||||||
setTimeout(() => {
|
window.require("electron").ipcRenderer.invoke("start-firebase-auth")
|
||||||
this.wasJustDragged = false;
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleInput(e) {
|
handleClose() {
|
||||||
this.apiKey = e.target.value;
|
console.log("Close button clicked")
|
||||||
this.errorMessage = '';
|
if (window.require) {
|
||||||
console.log('Input changed:', this.apiKey?.length || 0, 'chars');
|
window.require("electron").ipcRenderer.invoke("quit-application")
|
||||||
|
|
||||||
this.requestUpdate();
|
|
||||||
this.updateComplete.then(() => {
|
|
||||||
const inputField = this.shadowRoot?.querySelector('.apikey-input');
|
|
||||||
if (inputField && this.isInputFocused) {
|
|
||||||
inputField.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleProviderChange(e) {
|
handleAnimationEnd(e) {
|
||||||
this.selectedProvider = e.target.value;
|
if (e.target !== this) return
|
||||||
this.errorMessage = '';
|
|
||||||
console.log('Provider changed to:', this.selectedProvider);
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePaste(e) {
|
if (this.classList.contains("sliding-out")) {
|
||||||
e.preventDefault();
|
this.classList.remove("sliding-out")
|
||||||
this.errorMessage = '';
|
this.classList.add("hidden")
|
||||||
const clipboardText = (e.clipboardData || window.clipboardData).getData('text');
|
|
||||||
console.log('Paste event detected:', clipboardText?.substring(0, 10) + '...');
|
|
||||||
|
|
||||||
if (clipboardText) {
|
if (this.validatedApiKey) {
|
||||||
this.apiKey = clipboardText.trim();
|
|
||||||
|
|
||||||
const inputElement = e.target;
|
|
||||||
inputElement.value = this.apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.requestUpdate();
|
|
||||||
this.updateComplete.then(() => {
|
|
||||||
const inputField = this.shadowRoot?.querySelector('.apikey-input');
|
|
||||||
if (inputField) {
|
|
||||||
inputField.focus();
|
|
||||||
inputField.setSelectionRange(inputField.value.length, inputField.value.length);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyPress(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.handleSubmit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleSubmit() {
|
|
||||||
if (this.wasJustDragged || this.isLoading || !this.apiKey.trim()) {
|
|
||||||
console.log('Submit blocked:', {
|
|
||||||
wasJustDragged: this.wasJustDragged,
|
|
||||||
isLoading: this.isLoading,
|
|
||||||
hasApiKey: !!this.apiKey.trim(),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Starting API key validation...');
|
|
||||||
this.isLoading = true;
|
|
||||||
this.errorMessage = '';
|
|
||||||
this.requestUpdate();
|
|
||||||
|
|
||||||
const apiKey = this.apiKey.trim();
|
|
||||||
let isValid = false;
|
|
||||||
try {
|
|
||||||
const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider);
|
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
console.log('API key valid - starting slide out animation');
|
|
||||||
this.startSlideOutAnimation();
|
|
||||||
this.validatedApiKey = this.apiKey.trim();
|
|
||||||
this.validatedProvider = this.selectedProvider;
|
|
||||||
} else {
|
|
||||||
this.errorMessage = 'Invalid API key - please check and try again';
|
|
||||||
console.log('API key validation failed');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API key validation error:', error);
|
|
||||||
this.errorMessage = 'Validation error - please try again';
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateApiKey(apiKey, provider = 'openai') {
|
|
||||||
if (!apiKey || apiKey.length < 15) return false;
|
|
||||||
|
|
||||||
if (provider === 'openai') {
|
|
||||||
if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Validating OpenAI API key...');
|
|
||||||
|
|
||||||
const response = await fetch('https://api.openai.com/v1/models', {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const hasGPTModels = data.data && data.data.some(m => m.id.startsWith('gpt-'));
|
|
||||||
if (hasGPTModels) {
|
|
||||||
console.log('OpenAI API key validation successful');
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.log('API key valid but no GPT models available');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
console.log('API key validation failed:', response.status, errorData.error?.message || 'Unknown error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API key validation network error:', error);
|
|
||||||
return apiKey.length >= 20; // Fallback for network issues
|
|
||||||
}
|
|
||||||
} else if (provider === 'gemini') {
|
|
||||||
// Gemini API keys typically start with 'AIza'
|
|
||||||
if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Validating Gemini API key...');
|
|
||||||
|
|
||||||
// Test the API key with a simple models list request
|
|
||||||
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.models && data.models.length > 0) {
|
|
||||||
console.log('Gemini API key validation successful');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Gemini API key validation failed');
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Gemini API key validation network error:', error);
|
|
||||||
return apiKey.length >= 20; // Fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
startSlideOutAnimation() {
|
|
||||||
this.classList.add('sliding-out');
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUsePicklesKey(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.wasJustDragged) return;
|
|
||||||
|
|
||||||
console.log('Requesting Firebase authentication from main process...');
|
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
window.require('electron').ipcRenderer.invoke('start-firebase-auth');
|
window.require("electron").ipcRenderer.invoke("api-key-validated", {
|
||||||
|
apiKey: this.validatedApiKey,
|
||||||
|
provider: this.validatedProvider || "openai",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
this.validatedApiKey = null
|
||||||
|
this.validatedProvider = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleClose() {
|
connectedCallback() {
|
||||||
console.log('Close button clicked');
|
super.connectedCallback()
|
||||||
if (window.require) {
|
this.addEventListener("animationend", this.handleAnimationEnd)
|
||||||
window.require('electron').ipcRenderer.invoke('quit-application');
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAnimationEnd(e) {
|
disconnectedCallback() {
|
||||||
if (e.target !== this) return;
|
super.disconnectedCallback()
|
||||||
|
this.removeEventListener("animationend", this.handleAnimationEnd)
|
||||||
|
}
|
||||||
|
|
||||||
if (this.classList.contains('sliding-out')) {
|
render() {
|
||||||
this.classList.remove('sliding-out');
|
const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim()
|
||||||
this.classList.add('hidden');
|
console.log("Rendering with provider:", this.selectedProvider)
|
||||||
|
|
||||||
if (this.validatedApiKey) {
|
return html`
|
||||||
if (window.require) {
|
|
||||||
window.require('electron').ipcRenderer.invoke('api-key-validated', {
|
|
||||||
apiKey: this.validatedApiKey,
|
|
||||||
provider: this.validatedProvider || 'openai'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.validatedApiKey = null;
|
|
||||||
this.validatedProvider = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.addEventListener('animationend', this.handleAnimationEnd);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this.removeEventListener('animationend', this.handleAnimationEnd);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim();
|
|
||||||
console.log('Rendering with provider:', this.selectedProvider);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="container" @mousedown=${this.handleMouseDown}>
|
<div class="container" @mousedown=${this.handleMouseDown}>
|
||||||
<button class="close-button" @click=${this.handleClose} title="Close application">
|
<button class="close-button" @click=${this.handleClose} title="Close application">
|
||||||
<svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
|
<svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
|
||||||
@ -583,23 +615,30 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
<div class="provider-label">Select AI Provider:</div>
|
<div class="provider-label">Select AI Provider:</div>
|
||||||
<select
|
<select
|
||||||
class="provider-select"
|
class="provider-select"
|
||||||
.value=${this.selectedProvider || 'openai'}
|
.value=${this.selectedProvider || "openai"}
|
||||||
@change=${this.handleProviderChange}
|
@change=${this.handleProviderChange}
|
||||||
?disabled=${this.isLoading}
|
?disabled=${this.isLoading}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<option value="openai" ?selected=${this.selectedProvider === 'openai'}>OpenAI</option>
|
<option value="openai" ?selected=${this.selectedProvider === "openai"}>OpenAI</option>
|
||||||
<option value="gemini" ?selected=${this.selectedProvider === 'gemini'}>Google Gemini</option>
|
<option value="gemini" ?selected=${this.selectedProvider === "gemini"}>Google Gemini</option>
|
||||||
|
<option value="anthropic" ?selected=${this.selectedProvider === "anthropic"}>Anthropic</option>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
class="api-input"
|
class="api-input"
|
||||||
placeholder=${this.selectedProvider === 'openai' ? "Enter your OpenAI API key" : "Enter your Gemini API key"}
|
placeholder=${
|
||||||
.value=${this.apiKey || ''}
|
this.selectedProvider === "openai"
|
||||||
|
? "Enter your OpenAI API key"
|
||||||
|
: this.selectedProvider === "gemini"
|
||||||
|
? "Enter your Gemini API key"
|
||||||
|
: "Enter your Anthropic API key"
|
||||||
|
}
|
||||||
|
.value=${this.apiKey || ""}
|
||||||
@input=${this.handleInput}
|
@input=${this.handleInput}
|
||||||
@keypress=${this.handleKeyPress}
|
@keypress=${this.handleKeyPress}
|
||||||
@paste=${this.handlePaste}
|
@paste=${this.handlePaste}
|
||||||
@focus=${() => (this.errorMessage = '')}
|
@focus=${() => (this.errorMessage = "")}
|
||||||
?disabled=${this.isLoading}
|
?disabled=${this.isLoading}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
@ -607,7 +646,7 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button class="action-button" @click=${this.handleSubmit} ?disabled=${isButtonDisabled} tabindex="0">
|
<button class="action-button" @click=${this.handleSubmit} ?disabled=${isButtonDisabled} tabindex="0">
|
||||||
${this.isLoading ? 'Validating...' : 'Confirm'}
|
${this.isLoading ? "Validating..." : "Confirm"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="or-text">or</div>
|
<div class="or-text">or</div>
|
||||||
@ -615,8 +654,8 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
<button class="action-button" @click=${this.handleUsePicklesKey}>Use Pickle's API Key</button>
|
<button class="action-button" @click=${this.handleUsePicklesKey}>Use Pickle's API Key</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('apikey-header', ApiKeyHeader);
|
customElements.define("apikey-header", ApiKeyHeader)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
const providers = {
|
const providers = {
|
||||||
openai: require('./providers/openai'),
|
openai: require("./providers/openai"),
|
||||||
gemini: require('./providers/gemini'),
|
gemini: require("./providers/gemini"),
|
||||||
|
anthropic: require("./providers/anthropic"),
|
||||||
// 추가 provider는 여기에 등록
|
// 추가 provider는 여기에 등록
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an STT session based on provider
|
* Creates an STT session based on provider
|
||||||
@ -12,9 +13,9 @@ const providers = {
|
|||||||
*/
|
*/
|
||||||
function createSTT(provider, opts) {
|
function createSTT(provider, opts) {
|
||||||
if (!providers[provider]?.createSTT) {
|
if (!providers[provider]?.createSTT) {
|
||||||
throw new Error(`STT not supported for provider: ${provider}`);
|
throw new Error(`STT not supported for provider: ${provider}`)
|
||||||
}
|
}
|
||||||
return providers[provider].createSTT(opts);
|
return providers[provider].createSTT(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,9 +26,9 @@ function createSTT(provider, opts) {
|
|||||||
*/
|
*/
|
||||||
function createLLM(provider, opts) {
|
function createLLM(provider, opts) {
|
||||||
if (!providers[provider]?.createLLM) {
|
if (!providers[provider]?.createLLM) {
|
||||||
throw new Error(`LLM not supported for provider: ${provider}`);
|
throw new Error(`LLM not supported for provider: ${provider}`)
|
||||||
}
|
}
|
||||||
return providers[provider].createLLM(opts);
|
return providers[provider].createLLM(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,9 +39,9 @@ function createLLM(provider, opts) {
|
|||||||
*/
|
*/
|
||||||
function createStreamingLLM(provider, opts) {
|
function createStreamingLLM(provider, opts) {
|
||||||
if (!providers[provider]?.createStreamingLLM) {
|
if (!providers[provider]?.createStreamingLLM) {
|
||||||
throw new Error(`Streaming LLM not supported for provider: ${provider}`);
|
throw new Error(`Streaming LLM not supported for provider: ${provider}`)
|
||||||
}
|
}
|
||||||
return providers[provider].createStreamingLLM(opts);
|
return providers[provider].createStreamingLLM(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,20 +49,20 @@ function createStreamingLLM(provider, opts) {
|
|||||||
* @returns {object} Object with stt and llm arrays
|
* @returns {object} Object with stt and llm arrays
|
||||||
*/
|
*/
|
||||||
function getAvailableProviders() {
|
function getAvailableProviders() {
|
||||||
const sttProviders = [];
|
const sttProviders = []
|
||||||
const llmProviders = [];
|
const llmProviders = []
|
||||||
|
|
||||||
for (const [name, provider] of Object.entries(providers)) {
|
for (const [name, provider] of Object.entries(providers)) {
|
||||||
if (provider.createSTT) sttProviders.push(name);
|
if (provider.createSTT) sttProviders.push(name)
|
||||||
if (provider.createLLM) llmProviders.push(name);
|
if (provider.createLLM) llmProviders.push(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { stt: sttProviders, llm: llmProviders };
|
return { stt: sttProviders, llm: llmProviders }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createSTT,
|
createSTT,
|
||||||
createLLM,
|
createLLM,
|
||||||
createStreamingLLM,
|
createStreamingLLM,
|
||||||
getAvailableProviders
|
getAvailableProviders,
|
||||||
};
|
}
|
||||||
|
280
src/common/ai/providers/anthropic.js
Normal file
280
src/common/ai/providers/anthropic.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
const Anthropic = require("@anthropic-ai/sdk")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Anthropic STT session
|
||||||
|
* Note: Anthropic doesn't have native real-time STT, so this is a placeholder
|
||||||
|
* You might want to use a different STT service or implement a workaround
|
||||||
|
* @param {object} opts - Configuration options
|
||||||
|
* @param {string} opts.apiKey - Anthropic API key
|
||||||
|
* @param {string} [opts.language='en'] - Language code
|
||||||
|
* @param {object} [opts.callbacks] - Event callbacks
|
||||||
|
* @returns {Promise<object>} STT session placeholder
|
||||||
|
*/
|
||||||
|
async function createSTT({ apiKey, language = "en", callbacks = {}, ...config }) {
|
||||||
|
console.warn("[Anthropic] STT not natively supported. Consider using OpenAI or Gemini for STT.")
|
||||||
|
|
||||||
|
// Return a mock STT session that doesn't actually do anything
|
||||||
|
// You might want to fallback to another provider for STT
|
||||||
|
return {
|
||||||
|
sendRealtimeInput: async (audioData) => {
|
||||||
|
console.warn("[Anthropic] STT sendRealtimeInput called but not implemented")
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
console.log("[Anthropic] STT session closed")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Anthropic LLM instance
|
||||||
|
* @param {object} opts - Configuration options
|
||||||
|
* @param {string} opts.apiKey - Anthropic API key
|
||||||
|
* @param {string} [opts.model='claude-3-5-sonnet-20241022'] - Model name
|
||||||
|
* @param {number} [opts.temperature=0.7] - Temperature
|
||||||
|
* @param {number} [opts.maxTokens=4096] - Max tokens
|
||||||
|
* @returns {object} LLM instance
|
||||||
|
*/
|
||||||
|
function createLLM({ apiKey, model = "claude-3-5-sonnet-20241022", temperature = 0.7, maxTokens = 4096, ...config }) {
|
||||||
|
const client = new Anthropic({ apiKey })
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateContent: async (parts) => {
|
||||||
|
const messages = []
|
||||||
|
let systemPrompt = ""
|
||||||
|
const userContent = []
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (typeof part === "string") {
|
||||||
|
if (systemPrompt === "" && part.includes("You are")) {
|
||||||
|
systemPrompt = part
|
||||||
|
} else {
|
||||||
|
userContent.push({ type: "text", text: part })
|
||||||
|
}
|
||||||
|
} else if (part.inlineData) {
|
||||||
|
userContent.push({
|
||||||
|
type: "image",
|
||||||
|
source: {
|
||||||
|
type: "base64",
|
||||||
|
media_type: part.inlineData.mimeType,
|
||||||
|
data: part.inlineData.data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContent.length > 0) {
|
||||||
|
messages.push({ role: "user", content: userContent })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.messages.create({
|
||||||
|
model: model,
|
||||||
|
max_tokens: maxTokens,
|
||||||
|
temperature: temperature,
|
||||||
|
system: systemPrompt || undefined,
|
||||||
|
messages: messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: {
|
||||||
|
text: () => response.content[0].text,
|
||||||
|
},
|
||||||
|
raw: response,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Anthropic API error:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// For compatibility with chat-style interfaces
|
||||||
|
chat: async (messages) => {
|
||||||
|
let systemPrompt = ""
|
||||||
|
const anthropicMessages = []
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.role === "system") {
|
||||||
|
systemPrompt = msg.content
|
||||||
|
} else {
|
||||||
|
// Handle multimodal content
|
||||||
|
let content
|
||||||
|
if (Array.isArray(msg.content)) {
|
||||||
|
content = []
|
||||||
|
for (const part of msg.content) {
|
||||||
|
if (typeof part === "string") {
|
||||||
|
content.push({ type: "text", text: part })
|
||||||
|
} else if (part.type === "text") {
|
||||||
|
content.push({ type: "text", text: part.text })
|
||||||
|
} else if (part.type === "image_url" && part.image_url) {
|
||||||
|
// Convert base64 image to Anthropic format
|
||||||
|
const base64Data = part.image_url.url.split(",")[1]
|
||||||
|
content.push({
|
||||||
|
type: "image",
|
||||||
|
source: {
|
||||||
|
type: "base64",
|
||||||
|
media_type: "image/png",
|
||||||
|
data: base64Data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = [{ type: "text", text: msg.content }]
|
||||||
|
}
|
||||||
|
|
||||||
|
anthropicMessages.push({
|
||||||
|
role: msg.role === "user" ? "user" : "assistant",
|
||||||
|
content: content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.messages.create({
|
||||||
|
model: model,
|
||||||
|
max_tokens: maxTokens,
|
||||||
|
temperature: temperature,
|
||||||
|
system: systemPrompt || undefined,
|
||||||
|
messages: anthropicMessages,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: response.content[0].text,
|
||||||
|
raw: response,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Anthropic streaming LLM instance
|
||||||
|
* @param {object} opts - Configuration options
|
||||||
|
* @param {string} opts.apiKey - Anthropic API key
|
||||||
|
* @param {string} [opts.model='claude-3-5-sonnet-20241022'] - Model name
|
||||||
|
* @param {number} [opts.temperature=0.7] - Temperature
|
||||||
|
* @param {number} [opts.maxTokens=4096] - Max tokens
|
||||||
|
* @returns {object} Streaming LLM instance
|
||||||
|
*/
|
||||||
|
function createStreamingLLM({
|
||||||
|
apiKey,
|
||||||
|
model = "claude-3-5-sonnet-20241022",
|
||||||
|
temperature = 0.7,
|
||||||
|
maxTokens = 4096,
|
||||||
|
...config
|
||||||
|
}) {
|
||||||
|
const client = new Anthropic({ apiKey })
|
||||||
|
|
||||||
|
return {
|
||||||
|
streamChat: async (messages) => {
|
||||||
|
console.log("[Anthropic Provider] Starting streaming request")
|
||||||
|
|
||||||
|
let systemPrompt = ""
|
||||||
|
const anthropicMessages = []
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.role === "system") {
|
||||||
|
systemPrompt = msg.content
|
||||||
|
} else {
|
||||||
|
// Handle multimodal content
|
||||||
|
let content
|
||||||
|
if (Array.isArray(msg.content)) {
|
||||||
|
content = []
|
||||||
|
for (const part of msg.content) {
|
||||||
|
if (typeof part === "string") {
|
||||||
|
content.push({ type: "text", text: part })
|
||||||
|
} else if (part.type === "text") {
|
||||||
|
content.push({ type: "text", text: part.text })
|
||||||
|
} else if (part.type === "image_url" && part.image_url) {
|
||||||
|
// Convert base64 image to Anthropic format
|
||||||
|
const base64Data = part.image_url.url.split(",")[1]
|
||||||
|
content.push({
|
||||||
|
type: "image",
|
||||||
|
source: {
|
||||||
|
type: "base64",
|
||||||
|
media_type: "image/png",
|
||||||
|
data: base64Data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = [{ type: "text", text: msg.content }]
|
||||||
|
}
|
||||||
|
|
||||||
|
anthropicMessages.push({
|
||||||
|
role: msg.role === "user" ? "user" : "assistant",
|
||||||
|
content: content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a ReadableStream to handle Anthropic's streaming
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
try {
|
||||||
|
console.log("[Anthropic Provider] Processing messages:", anthropicMessages.length, "messages")
|
||||||
|
|
||||||
|
let chunkCount = 0
|
||||||
|
let totalContent = ""
|
||||||
|
|
||||||
|
// Stream the response
|
||||||
|
const stream = await client.messages.create({
|
||||||
|
model: model,
|
||||||
|
max_tokens: maxTokens,
|
||||||
|
temperature: temperature,
|
||||||
|
system: systemPrompt || undefined,
|
||||||
|
messages: anthropicMessages,
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
|
||||||
|
chunkCount++
|
||||||
|
const chunkText = chunk.delta.text || ""
|
||||||
|
totalContent += chunkText
|
||||||
|
|
||||||
|
// Format as SSE data
|
||||||
|
const data = JSON.stringify({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
delta: {
|
||||||
|
content: chunkText,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Anthropic Provider] Streamed ${chunkCount} chunks, total length: ${totalContent.length} chars`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send the final done message
|
||||||
|
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||||
|
controller.close()
|
||||||
|
console.log("[Anthropic Provider] Streaming completed successfully")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Anthropic Provider] Streaming error:", error)
|
||||||
|
controller.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a Response object with the stream
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createSTT,
|
||||||
|
createLLM,
|
||||||
|
createStreamingLLM,
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron');
|
const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron');
|
||||||
const WindowLayoutManager = require('./windowLayoutManager');
|
const WindowLayoutManager = require('./windowLayoutManager');
|
||||||
const SmoothMovementManager = require('./smoothMovementManager');
|
const SmoothMovementManager = require('./smoothMovementManager');
|
||||||
const liquidGlass = require('electron-liquid-glass');
|
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
@ -15,6 +14,7 @@ const fetch = require('node-fetch');
|
|||||||
|
|
||||||
|
|
||||||
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
||||||
|
let liquidGlass;
|
||||||
const isLiquidGlassSupported = () => {
|
const isLiquidGlassSupported = () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
return false;
|
return false;
|
||||||
@ -23,7 +23,15 @@ const isLiquidGlassSupported = () => {
|
|||||||
// return majorVersion >= 25; // macOS 26+ (Darwin 25+)
|
// return majorVersion >= 25; // macOS 26+ (Darwin 25+)
|
||||||
return majorVersion >= 26; // See you soon!
|
return majorVersion >= 26; // See you soon!
|
||||||
};
|
};
|
||||||
const shouldUseLiquidGlass = isLiquidGlassSupported();
|
let shouldUseLiquidGlass = isLiquidGlassSupported();
|
||||||
|
if (shouldUseLiquidGlass) {
|
||||||
|
try {
|
||||||
|
liquidGlass = require('electron-liquid-glass');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not load optional dependency "electron-liquid-glass". The feature will be disabled.');
|
||||||
|
shouldUseLiquidGlass = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
||||||
|
|
||||||
let isContentProtectionOn = true;
|
let isContentProtectionOn = true;
|
||||||
@ -83,7 +91,9 @@ function createFeatureWindows(header) {
|
|||||||
});
|
});
|
||||||
listen.setContentProtection(isContentProtectionOn);
|
listen.setContentProtection(isContentProtectionOn);
|
||||||
listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||||
listen.setWindowButtonVisibility(false);
|
if (process.platform === 'darwin') {
|
||||||
|
listen.setWindowButtonVisibility(false);
|
||||||
|
}
|
||||||
const listenLoadOptions = { query: { view: 'listen' } };
|
const listenLoadOptions = { query: { view: 'listen' } };
|
||||||
if (!shouldUseLiquidGlass) {
|
if (!shouldUseLiquidGlass) {
|
||||||
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
||||||
@ -112,7 +122,9 @@ function createFeatureWindows(header) {
|
|||||||
const ask = new BrowserWindow({ ...commonChildOptions, width:600 });
|
const ask = new BrowserWindow({ ...commonChildOptions, width:600 });
|
||||||
ask.setContentProtection(isContentProtectionOn);
|
ask.setContentProtection(isContentProtectionOn);
|
||||||
ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||||
ask.setWindowButtonVisibility(false);
|
if (process.platform === 'darwin') {
|
||||||
|
ask.setWindowButtonVisibility(false);
|
||||||
|
}
|
||||||
const askLoadOptions = { query: { view: 'ask' } };
|
const askLoadOptions = { query: { view: 'ask' } };
|
||||||
if (!shouldUseLiquidGlass) {
|
if (!shouldUseLiquidGlass) {
|
||||||
ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions);
|
ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions);
|
||||||
@ -146,7 +158,9 @@ function createFeatureWindows(header) {
|
|||||||
const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined });
|
const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined });
|
||||||
settings.setContentProtection(isContentProtectionOn);
|
settings.setContentProtection(isContentProtectionOn);
|
||||||
settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||||
settings.setWindowButtonVisibility(false);
|
if (process.platform === 'darwin') {
|
||||||
|
settings.setWindowButtonVisibility(false);
|
||||||
|
}
|
||||||
const settingsLoadOptions = { query: { view: 'settings' } };
|
const settingsLoadOptions = { query: { view: 'settings' } };
|
||||||
if (!shouldUseLiquidGlass) {
|
if (!shouldUseLiquidGlass) {
|
||||||
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
||||||
@ -236,12 +250,13 @@ function toggleAllWindowsVisibility(movementManager) {
|
|||||||
if (win.isVisible()) {
|
if (win.isVisible()) {
|
||||||
lastVisibleWindows.add(name);
|
lastVisibleWindows.add(name);
|
||||||
if (name !== 'header') {
|
if (name !== 'header') {
|
||||||
win.webContents.send('window-hide-animation');
|
// win.webContents.send('window-hide-animation');
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
if (!win.isDestroyed()) {
|
// if (!win.isDestroyed()) {
|
||||||
win.hide();
|
// win.hide();
|
||||||
}
|
// }
|
||||||
}, 200);
|
// }, 200);
|
||||||
|
win.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -251,7 +266,7 @@ function toggleAllWindowsVisibility(movementManager) {
|
|||||||
movementManager.hideToEdge(nearestEdge, () => {
|
movementManager.hideToEdge(nearestEdge, () => {
|
||||||
header.hide();
|
header.hide();
|
||||||
console.log('[Visibility] Smart hide completed');
|
console.log('[Visibility] Smart hide completed');
|
||||||
});
|
}, { instant: true });
|
||||||
} else {
|
} else {
|
||||||
console.log('[Visibility] Smart showing from hidden position');
|
console.log('[Visibility] Smart showing from hidden position');
|
||||||
console.log('[Visibility] Restoring windows:', Array.from(lastVisibleWindows));
|
console.log('[Visibility] Restoring windows:', Array.from(lastVisibleWindows));
|
||||||
@ -306,7 +321,9 @@ function createWindows() {
|
|||||||
webSecurity: false,
|
webSecurity: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
header.setWindowButtonVisibility(false);
|
if (process.platform === 'darwin') {
|
||||||
|
header.setWindowButtonVisibility(false);
|
||||||
|
}
|
||||||
const headerLoadOptions = {};
|
const headerLoadOptions = {};
|
||||||
if (!shouldUseLiquidGlass) {
|
if (!shouldUseLiquidGlass) {
|
||||||
header.loadFile(path.join(__dirname, '../app/header.html'), headerLoadOptions);
|
header.loadFile(path.join(__dirname, '../app/header.html'), headerLoadOptions);
|
||||||
@ -372,7 +389,7 @@ function createWindows() {
|
|||||||
// loadAndRegisterShortcuts();
|
// loadAndRegisterShortcuts();
|
||||||
// });
|
// });
|
||||||
|
|
||||||
ipcMain.handle('toggle-all-windows-visibility', toggleAllWindowsVisibility);
|
ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager));
|
||||||
|
|
||||||
ipcMain.handle('toggle-feature', async (event, featureName) => {
|
ipcMain.handle('toggle-feature', async (event, featureName) => {
|
||||||
if (!windowPool.get(featureName) && currentHeaderState === 'main') {
|
if (!windowPool.get(featureName) && currentHeaderState === 'main') {
|
||||||
@ -1482,4 +1499,4 @@ module.exports = {
|
|||||||
getStoredApiKey,
|
getStoredApiKey,
|
||||||
getStoredProvider,
|
getStoredProvider,
|
||||||
captureScreenshot,
|
captureScreenshot,
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user