Feature:Anthropic AI Integration
This commit is contained in:
		
							parent
							
								
									6bbca03719
								
							
						
					
					
						commit
						17b10b1ad0
					
				@ -29,6 +29,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "license": "GPL-3.0",
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@anthropic-ai/sdk": "^0.56.0",
 | 
			
		||||
        "@google/genai": "^1.8.0",
 | 
			
		||||
        "@google/generative-ai": "^0.24.1",
 | 
			
		||||
        "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 {
 | 
			
		||||
    static properties = {
 | 
			
		||||
        apiKey: { type: String },
 | 
			
		||||
        isLoading: { type: Boolean },
 | 
			
		||||
        errorMessage: { type: String },
 | 
			
		||||
        selectedProvider: { type: String },
 | 
			
		||||
    };
 | 
			
		||||
  static properties = {
 | 
			
		||||
    apiKey: { type: String },
 | 
			
		||||
    isLoading: { type: Boolean },
 | 
			
		||||
    errorMessage: { type: String },
 | 
			
		||||
    selectedProvider: { type: String },
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    static styles = css`
 | 
			
		||||
  static styles = css`
 | 
			
		||||
        :host {
 | 
			
		||||
            display: block;
 | 
			
		||||
            transform: translate3d(0, 0, 0);
 | 
			
		||||
@ -272,304 +272,336 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
        :host-context(body.has-glass) .close-button:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.dragState = null;
 | 
			
		||||
        this.wasJustDragged = false;
 | 
			
		||||
        this.apiKey = '';
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        this.errorMessage = '';
 | 
			
		||||
        this.validatedApiKey = null;
 | 
			
		||||
        this.selectedProvider = 'openai';
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super()
 | 
			
		||||
    this.dragState = null
 | 
			
		||||
    this.wasJustDragged = false
 | 
			
		||||
    this.apiKey = ""
 | 
			
		||||
    this.isLoading = false
 | 
			
		||||
    this.errorMessage = ""
 | 
			
		||||
    this.validatedApiKey = null
 | 
			
		||||
    this.selectedProvider = "openai"
 | 
			
		||||
 | 
			
		||||
        this.handleMouseMove = this.handleMouseMove.bind(this);
 | 
			
		||||
        this.handleMouseUp = this.handleMouseUp.bind(this);
 | 
			
		||||
        this.handleKeyPress = this.handleKeyPress.bind(this);
 | 
			
		||||
        this.handleSubmit = this.handleSubmit.bind(this);
 | 
			
		||||
        this.handleInput = this.handleInput.bind(this);
 | 
			
		||||
        this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
 | 
			
		||||
        this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this);
 | 
			
		||||
        this.handleProviderChange = this.handleProviderChange.bind(this);
 | 
			
		||||
    this.handleMouseMove = this.handleMouseMove.bind(this)
 | 
			
		||||
    this.handleMouseUp = this.handleMouseUp.bind(this)
 | 
			
		||||
    this.handleKeyPress = this.handleKeyPress.bind(this)
 | 
			
		||||
    this.handleSubmit = this.handleSubmit.bind(this)
 | 
			
		||||
    this.handleInput = this.handleInput.bind(this)
 | 
			
		||||
    this.handleAnimationEnd = this.handleAnimationEnd.bind(this)
 | 
			
		||||
    this.handleUsePicklesKey = this.handleUsePicklesKey.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() {
 | 
			
		||||
        this.apiKey = '';
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        this.errorMessage = '';
 | 
			
		||||
        this.validatedApiKey = null;
 | 
			
		||||
        this.selectedProvider = 'openai';
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
 | 
			
		||||
    const { ipcRenderer } = window.require("electron")
 | 
			
		||||
    const initialPosition = await ipcRenderer.invoke("get-header-position")
 | 
			
		||||
 | 
			
		||||
    this.dragState = {
 | 
			
		||||
      initialMouseX: e.screenX,
 | 
			
		||||
      initialMouseY: e.screenY,
 | 
			
		||||
      initialWindowX: initialPosition.x,
 | 
			
		||||
      initialWindowY: initialPosition.y,
 | 
			
		||||
      moved: false,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleMouseDown(e) {
 | 
			
		||||
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') {
 | 
			
		||||
            return;
 | 
			
		||||
    window.addEventListener("mousemove", this.handleMouseMove)
 | 
			
		||||
    window.addEventListener("mouseup", this.handleMouseUp, { once: 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)
 | 
			
		||||
    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');
 | 
			
		||||
        const initialPosition = await ipcRenderer.invoke('get-header-position');
 | 
			
		||||
      try {
 | 
			
		||||
        console.log("Validating Anthropic API key...")
 | 
			
		||||
 | 
			
		||||
        this.dragState = {
 | 
			
		||||
            initialMouseX: e.screenX,
 | 
			
		||||
            initialMouseY: e.screenY,
 | 
			
		||||
            initialWindowX: initialPosition.x,
 | 
			
		||||
            initialWindowY: initialPosition.y,
 | 
			
		||||
            moved: false,
 | 
			
		||||
        };
 | 
			
		||||
        // Test the API key with a simple request
 | 
			
		||||
        const response = await fetch("https://api.anthropic.com/v1/messages", {
 | 
			
		||||
          method: "POST",
 | 
			
		||||
          headers: {
 | 
			
		||||
            "Content-Type": "application/json",
 | 
			
		||||
            "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);
 | 
			
		||||
        window.addEventListener('mouseup', this.handleMouseUp, { once: 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;
 | 
			
		||||
        if (response.ok || response.status === 400) {
 | 
			
		||||
          // 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
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
        console.log("Anthropic API key validation failed:", response.status)
 | 
			
		||||
        return false
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error("Anthropic API key validation network error:", error)
 | 
			
		||||
        return apiKey.length >= 20 // Fallback
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseUp(e) {
 | 
			
		||||
        if (!this.dragState) return;
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
        const wasDragged = this.dragState.moved;
 | 
			
		||||
  startSlideOutAnimation() {
 | 
			
		||||
    this.classList.add("sliding-out")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
        window.removeEventListener('mousemove', this.handleMouseMove);
 | 
			
		||||
        this.dragState = null;
 | 
			
		||||
  handleUsePicklesKey(e) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    if (this.wasJustDragged) return
 | 
			
		||||
 | 
			
		||||
        if (wasDragged) {
 | 
			
		||||
            this.wasJustDragged = true;
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                this.wasJustDragged = false;
 | 
			
		||||
            }, 200);
 | 
			
		||||
        }
 | 
			
		||||
    console.log("Requesting Firebase authentication from main process...")
 | 
			
		||||
    if (window.require) {
 | 
			
		||||
      window.require("electron").ipcRenderer.invoke("start-firebase-auth")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
  handleClose() {
 | 
			
		||||
    console.log("Close button clicked")
 | 
			
		||||
    if (window.require) {
 | 
			
		||||
      window.require("electron").ipcRenderer.invoke("quit-application")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    handleProviderChange(e) {
 | 
			
		||||
        this.selectedProvider = e.target.value;
 | 
			
		||||
        this.errorMessage = '';
 | 
			
		||||
        console.log('Provider changed to:', this.selectedProvider);
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
  handleAnimationEnd(e) {
 | 
			
		||||
    if (e.target !== this) return
 | 
			
		||||
 | 
			
		||||
    handlePaste(e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        this.errorMessage = '';
 | 
			
		||||
        const clipboardText = (e.clipboardData || window.clipboardData).getData('text');
 | 
			
		||||
        console.log('Paste event detected:', clipboardText?.substring(0, 10) + '...');
 | 
			
		||||
    if (this.classList.contains("sliding-out")) {
 | 
			
		||||
      this.classList.remove("sliding-out")
 | 
			
		||||
      this.classList.add("hidden")
 | 
			
		||||
 | 
			
		||||
        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();
 | 
			
		||||
        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 (this.validatedApiKey) {
 | 
			
		||||
        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() {
 | 
			
		||||
        console.log('Close button clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            window.require('electron').ipcRenderer.invoke('quit-application');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  connectedCallback() {
 | 
			
		||||
    super.connectedCallback()
 | 
			
		||||
    this.addEventListener("animationend", this.handleAnimationEnd)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    handleAnimationEnd(e) {
 | 
			
		||||
        if (e.target !== this) return;
 | 
			
		||||
  disconnectedCallback() {
 | 
			
		||||
    super.disconnectedCallback()
 | 
			
		||||
    this.removeEventListener("animationend", this.handleAnimationEnd)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
        if (this.classList.contains('sliding-out')) {
 | 
			
		||||
            this.classList.remove('sliding-out');
 | 
			
		||||
            this.classList.add('hidden');
 | 
			
		||||
  render() {
 | 
			
		||||
    const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim()
 | 
			
		||||
    console.log("Rendering with provider:", this.selectedProvider)
 | 
			
		||||
 | 
			
		||||
            if (this.validatedApiKey) {
 | 
			
		||||
                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`
 | 
			
		||||
    return html`
 | 
			
		||||
            <div class="container" @mousedown=${this.handleMouseDown}>
 | 
			
		||||
                <button class="close-button" @click=${this.handleClose} title="Close application">
 | 
			
		||||
                    <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>
 | 
			
		||||
                    <select
 | 
			
		||||
                        class="provider-select"
 | 
			
		||||
                        .value=${this.selectedProvider || 'openai'}
 | 
			
		||||
                        .value=${this.selectedProvider || "openai"}
 | 
			
		||||
                        @change=${this.handleProviderChange}
 | 
			
		||||
                        ?disabled=${this.isLoading}
 | 
			
		||||
                        tabindex="0"
 | 
			
		||||
                    >
 | 
			
		||||
                        <option value="openai" ?selected=${this.selectedProvider === 'openai'}>OpenAI</option>
 | 
			
		||||
                        <option value="gemini" ?selected=${this.selectedProvider === 'gemini'}>Google Gemini</option>
 | 
			
		||||
                        <option value="openai" ?selected=${this.selectedProvider === "openai"}>OpenAI</option>
 | 
			
		||||
                        <option value="gemini" ?selected=${this.selectedProvider === "gemini"}>Google Gemini</option>
 | 
			
		||||
                        <option value="anthropic" ?selected=${this.selectedProvider === "anthropic"}>Anthropic</option>
 | 
			
		||||
                    </select>
 | 
			
		||||
                    <input
 | 
			
		||||
                        type="password"
 | 
			
		||||
                        class="api-input"
 | 
			
		||||
                        placeholder=${this.selectedProvider === 'openai' ? "Enter your OpenAI API key" : "Enter your Gemini API key"}
 | 
			
		||||
                        .value=${this.apiKey || ''}
 | 
			
		||||
                        placeholder=${
 | 
			
		||||
                          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}
 | 
			
		||||
                        @keypress=${this.handleKeyPress}
 | 
			
		||||
                        @paste=${this.handlePaste}
 | 
			
		||||
                        @focus=${() => (this.errorMessage = '')}
 | 
			
		||||
                        @focus=${() => (this.errorMessage = "")}
 | 
			
		||||
                        ?disabled=${this.isLoading}
 | 
			
		||||
                        autocomplete="off"
 | 
			
		||||
                        spellcheck="false"
 | 
			
		||||
@ -607,7 +646,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
                    />
 | 
			
		||||
 | 
			
		||||
                    <button class="action-button" @click=${this.handleSubmit} ?disabled=${isButtonDisabled} tabindex="0">
 | 
			
		||||
                        ${this.isLoading ? 'Validating...' : 'Confirm'}
 | 
			
		||||
                        ${this.isLoading ? "Validating..." : "Confirm"}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
                    <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>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
        `
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define('apikey-header', ApiKeyHeader);
 | 
			
		||||
customElements.define("apikey-header", ApiKeyHeader)
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
const providers = {
 | 
			
		||||
  openai: require('./providers/openai'),
 | 
			
		||||
  gemini: require('./providers/gemini'),
 | 
			
		||||
  openai: require("./providers/openai"),
 | 
			
		||||
  gemini: require("./providers/gemini"),
 | 
			
		||||
  anthropic: require("./providers/anthropic"),
 | 
			
		||||
  // 추가 provider는 여기에 등록
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates an STT session based on provider
 | 
			
		||||
@ -12,9 +13,9 @@ const providers = {
 | 
			
		||||
 */
 | 
			
		||||
function createSTT(provider, opts) {
 | 
			
		||||
  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) {
 | 
			
		||||
  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) {
 | 
			
		||||
  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
 | 
			
		||||
 */
 | 
			
		||||
function getAvailableProviders() {
 | 
			
		||||
  const sttProviders = [];
 | 
			
		||||
  const llmProviders = [];
 | 
			
		||||
  
 | 
			
		||||
  const sttProviders = []
 | 
			
		||||
  const llmProviders = []
 | 
			
		||||
 | 
			
		||||
  for (const [name, provider] of Object.entries(providers)) {
 | 
			
		||||
    if (provider.createSTT) sttProviders.push(name);
 | 
			
		||||
    if (provider.createLLM) llmProviders.push(name);
 | 
			
		||||
    if (provider.createSTT) sttProviders.push(name)
 | 
			
		||||
    if (provider.createLLM) llmProviders.push(name)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return { stt: sttProviders, llm: llmProviders };
 | 
			
		||||
 | 
			
		||||
  return { stt: sttProviders, llm: llmProviders }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  createSTT,
 | 
			
		||||
  createLLM,
 | 
			
		||||
  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 WindowLayoutManager = require('./windowLayoutManager');
 | 
			
		||||
const SmoothMovementManager = require('./smoothMovementManager');
 | 
			
		||||
const liquidGlass = require('electron-liquid-glass');
 | 
			
		||||
const path = require('node:path');
 | 
			
		||||
const fs = require('node:fs');
 | 
			
		||||
const os = require('os');
 | 
			
		||||
@ -15,6 +14,7 @@ const fetch = require('node-fetch');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* ────────────────[ GLASS BYPASS ]─────────────── */
 | 
			
		||||
let liquidGlass;
 | 
			
		||||
const isLiquidGlassSupported = () => {
 | 
			
		||||
    if (process.platform !== 'darwin') {
 | 
			
		||||
        return false;
 | 
			
		||||
@ -23,7 +23,15 @@ const isLiquidGlassSupported = () => {
 | 
			
		||||
    // return majorVersion >= 25; // macOS 26+ (Darwin 25+)
 | 
			
		||||
    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 ]─────────────── */
 | 
			
		||||
 | 
			
		||||
let isContentProtectionOn = true;
 | 
			
		||||
@ -83,7 +91,9 @@ function createFeatureWindows(header) {
 | 
			
		||||
    });
 | 
			
		||||
    listen.setContentProtection(isContentProtectionOn);
 | 
			
		||||
    listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
 | 
			
		||||
    listen.setWindowButtonVisibility(false);
 | 
			
		||||
    if (process.platform === 'darwin') {
 | 
			
		||||
        listen.setWindowButtonVisibility(false);
 | 
			
		||||
    }
 | 
			
		||||
    const listenLoadOptions = { query: { view: 'listen' } };
 | 
			
		||||
    if (!shouldUseLiquidGlass) {
 | 
			
		||||
        listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
 | 
			
		||||
@ -112,7 +122,9 @@ function createFeatureWindows(header) {
 | 
			
		||||
    const ask = new BrowserWindow({ ...commonChildOptions, width:600 });
 | 
			
		||||
    ask.setContentProtection(isContentProtectionOn);
 | 
			
		||||
    ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
 | 
			
		||||
    ask.setWindowButtonVisibility(false);
 | 
			
		||||
    if (process.platform === 'darwin') {
 | 
			
		||||
        ask.setWindowButtonVisibility(false);
 | 
			
		||||
    }
 | 
			
		||||
    const askLoadOptions = { query: { view: 'ask' } };
 | 
			
		||||
    if (!shouldUseLiquidGlass) {
 | 
			
		||||
        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 });
 | 
			
		||||
    settings.setContentProtection(isContentProtectionOn);
 | 
			
		||||
    settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
 | 
			
		||||
    settings.setWindowButtonVisibility(false);
 | 
			
		||||
    if (process.platform === 'darwin') {
 | 
			
		||||
        settings.setWindowButtonVisibility(false);
 | 
			
		||||
    }
 | 
			
		||||
    const settingsLoadOptions = { query: { view: 'settings' } };
 | 
			
		||||
    if (!shouldUseLiquidGlass) {
 | 
			
		||||
        settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
 | 
			
		||||
@ -236,12 +250,13 @@ function toggleAllWindowsVisibility(movementManager) {
 | 
			
		||||
            if (win.isVisible()) {
 | 
			
		||||
                lastVisibleWindows.add(name);
 | 
			
		||||
                if (name !== 'header') {
 | 
			
		||||
                    win.webContents.send('window-hide-animation');
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        if (!win.isDestroyed()) {
 | 
			
		||||
                            win.hide();
 | 
			
		||||
                        }
 | 
			
		||||
                    }, 200);
 | 
			
		||||
                    // win.webContents.send('window-hide-animation');
 | 
			
		||||
                    // setTimeout(() => {
 | 
			
		||||
                    //     if (!win.isDestroyed()) {
 | 
			
		||||
                    //         win.hide();
 | 
			
		||||
                    //     }
 | 
			
		||||
                    // }, 200);
 | 
			
		||||
                    win.hide();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
@ -251,7 +266,7 @@ function toggleAllWindowsVisibility(movementManager) {
 | 
			
		||||
        movementManager.hideToEdge(nearestEdge, () => {
 | 
			
		||||
            header.hide();
 | 
			
		||||
            console.log('[Visibility] Smart hide completed');
 | 
			
		||||
        });
 | 
			
		||||
        }, { instant: true });
 | 
			
		||||
    } else {
 | 
			
		||||
        console.log('[Visibility] Smart showing from hidden position');
 | 
			
		||||
        console.log('[Visibility] Restoring windows:', Array.from(lastVisibleWindows));
 | 
			
		||||
@ -306,7 +321,9 @@ function createWindows() {
 | 
			
		||||
            webSecurity: false,
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
    header.setWindowButtonVisibility(false);
 | 
			
		||||
    if (process.platform === 'darwin') {
 | 
			
		||||
        header.setWindowButtonVisibility(false);
 | 
			
		||||
    }
 | 
			
		||||
    const headerLoadOptions = {};
 | 
			
		||||
    if (!shouldUseLiquidGlass) {
 | 
			
		||||
        header.loadFile(path.join(__dirname, '../app/header.html'), headerLoadOptions);
 | 
			
		||||
@ -372,7 +389,7 @@ function createWindows() {
 | 
			
		||||
    //     loadAndRegisterShortcuts();
 | 
			
		||||
    // });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('toggle-all-windows-visibility', toggleAllWindowsVisibility);
 | 
			
		||||
    ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager));
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('toggle-feature', async (event, featureName) => {
 | 
			
		||||
        if (!windowPool.get(featureName) && currentHeaderState === 'main') {
 | 
			
		||||
@ -1482,4 +1499,4 @@ module.exports = {
 | 
			
		||||
    getStoredApiKey,
 | 
			
		||||
    getStoredProvider,
 | 
			
		||||
    captureScreenshot,
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user