mirror of
https://github.com/katanemo/plano.git
synced 2026-06-17 15:25:17 +02:00
removing redunant demos
This commit is contained in:
parent
4b4ba50e5f
commit
6aac41a729
24 changed files with 0 additions and 20123 deletions
|
|
@ -1,19 +0,0 @@
|
|||
FROM python:3.12 AS base
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY requirements.txt /src/
|
||||
RUN pip install --prefix=/runtime --force-reinstall -r requirements.txt
|
||||
|
||||
COPY ../. /src
|
||||
|
||||
FROM python:3.12-slim AS output
|
||||
|
||||
COPY --from=builder /runtime /usr/local
|
||||
|
||||
COPY ../. /app
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80", "--log-level", "info"]
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# Network Agent Demo
|
||||
|
||||
This demo illustrates how **Arch** can be used to perform function calling with network-related tasks. In this demo, you act as a **network assistant** that provides factual information, without offering advice on manufacturers or purchasing decisions.
|
||||
|
||||
The assistant can perform several key operations, including rebooting devices, answering general networking questions, and retrieving device statistics. By default, the system prompt ensures that the assistant's responses are factual and neutral.
|
||||
|
||||
## Available Functions:
|
||||
- **Reboot Devices**: Allows rebooting specific devices or device groups, with an optional time range for scheduling the reboot.
|
||||
- Parameters:
|
||||
- `device_ids` (required): A list of device IDs to reboot.
|
||||
- `time_range` (optional): Specifies the time range in days, defaulting to 7 days if not provided.
|
||||
|
||||
- **Network Q/A**: Handles general Q&A related to networking. This function is the default target for general networking queries.
|
||||
|
||||
- **Device Summary**: Retrieves statistics for specific devices within a given time range.
|
||||
- Parameters:
|
||||
- `device_ids` (required): A list of device IDs for which statistics will be retrieved.
|
||||
- `time_range` (optional): Specifies the time range in days for gathering statistics, with a default of 7 days.
|
||||
|
||||
|
||||
# Starting the demo
|
||||
1. Please make sure the [pre-requisites](https://github.com/katanemo/arch/?tab=readme-ov-file#prerequisites) are installed correctly
|
||||
2. Start Arch
|
||||
```sh
|
||||
sh run_demo.sh
|
||||
```
|
||||
3. Navigate to http://localhost:18080/agent/chat
|
||||
4. Tell me what can you do for me?"
|
||||
|
||||
# Observability
|
||||
Arch gateway publishes stats endpoint at http://localhost:19901/stats. In this demo we are using prometheus to pull stats from arch and we are using grafana to visualize the stats in dashboard. To see grafana dashboard follow instructions below,
|
||||
|
||||
1. Start grafana and prometheus using following command
|
||||
```yaml
|
||||
docker compose --profile monitoring up
|
||||
```
|
||||
1. Navigate to http://localhost:3000/ to open grafana UI (use admin/grafana as credentials)
|
||||
1. From grafana left nav click on dashboards and select "Intelligent Gateway Overview" to view arch gateway stats
|
||||
|
||||
Here is sample interaction
|
||||
|
||||

|
||||
|
|
@ -1,61 +0,0 @@
|
|||
version: v0.1.0
|
||||
listeners:
|
||||
ingress_traffic:
|
||||
address: 0.0.0.0
|
||||
port: 10000
|
||||
message_format: openai
|
||||
timeout: 30s
|
||||
|
||||
# Centralized way to manage LLMs, manage keys, retry logic, failover and limits in a central way
|
||||
llm_providers:
|
||||
- access_key: $OPENAI_API_KEY
|
||||
model: openai/gpt-4o
|
||||
default: true
|
||||
|
||||
# default system prompt used by all prompt targets
|
||||
system_prompt: |
|
||||
You are a network assistant that helps operators with a better understanding of network traffic flow and perform actions on networking operations. No advice on manufacturers or purchasing decisions.
|
||||
|
||||
prompt_targets:
|
||||
- name: device_summary
|
||||
description: Retrieve network statistics for specific devices within a time range
|
||||
endpoint:
|
||||
name: app_server
|
||||
path: /agent/device_summary
|
||||
http_method: POST
|
||||
parameters:
|
||||
- name: device_id
|
||||
type: str
|
||||
description: A device identifier to retrieve statistics for.
|
||||
required: true # device_ids are required to get device statistics
|
||||
- name: days
|
||||
type: int
|
||||
description: The number of days for which to gather device statistics.
|
||||
default: 7
|
||||
- name: reboot_device
|
||||
description: Reboot a device
|
||||
endpoint:
|
||||
name: app_server
|
||||
path: /agent/device_reboot
|
||||
http_method: POST
|
||||
parameters:
|
||||
- name: device_id
|
||||
type: str
|
||||
description: the device identifier
|
||||
required: true
|
||||
system_prompt: You will get a status JSON object. Simply summarize it
|
||||
|
||||
# Arch creates a round-robin load balancing between different endpoints, managed via the cluster subsystem.
|
||||
endpoints:
|
||||
app_server:
|
||||
# value could be ip address or a hostname with port
|
||||
# this could also be a list of endpoints for load balancing
|
||||
# for example endpoint: [ ip1:port, ip2:port ]
|
||||
endpoint: host.docker.internal:18083
|
||||
# max time to wait for a connection to be established
|
||||
connect_timeout: 0.005s
|
||||
|
||||
|
||||
tracing:
|
||||
random_sampling: 100
|
||||
trace_arch_internal: true
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
services:
|
||||
api_server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "18083:80"
|
||||
|
||||
chatbot_ui:
|
||||
build:
|
||||
context: ../../shared/chatbot_ui
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "18080:8080"
|
||||
environment:
|
||||
- CHAT_COMPLETION_ENDPOINT=http://host.docker.internal:10000/v1
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ./arch_config.yaml:/app/arch_config.yaml
|
||||
|
||||
jaeger:
|
||||
build:
|
||||
context: ../../shared/jaeger
|
||||
ports:
|
||||
- "16686:16686"
|
||||
- "4317:4317"
|
||||
- "4318:4318"
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 636 KiB |
|
|
@ -1,92 +0,0 @@
|
|||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from openai import OpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
app = FastAPI()
|
||||
DEMO_DESCRIPTION = """This demo illustrates how **Arch** can be used to perform function calling
|
||||
with network-related tasks. In this demo, you act as a **network assistant** that provides factual
|
||||
information, without offering advice on manufacturers or purchasing decisions."""
|
||||
|
||||
|
||||
# Define the request model
|
||||
class DeviceSummaryRequest(BaseModel):
|
||||
device_id: str
|
||||
time_range: Optional[int] = Field(
|
||||
default=7, description="Time range in days, defaults to 7"
|
||||
)
|
||||
|
||||
|
||||
# Define the response model
|
||||
class DeviceStatistics(BaseModel):
|
||||
device_id: str
|
||||
time_range: str
|
||||
data: str
|
||||
|
||||
|
||||
class DeviceSummaryResponse(BaseModel):
|
||||
statistics: List[DeviceStatistics]
|
||||
|
||||
# Request model for device reboot
|
||||
|
||||
|
||||
class DeviceRebootRequest(BaseModel):
|
||||
device_id: str
|
||||
|
||||
|
||||
# Response model for the device reboot
|
||||
class CoverageResponse(BaseModel):
|
||||
status: str
|
||||
summary: dict
|
||||
|
||||
|
||||
@app.post("/agent/device_reboot", response_model=CoverageResponse)
|
||||
def reboot_network_device(request_data: DeviceRebootRequest):
|
||||
"""
|
||||
Endpoint to reboot network devices based on device IDs and an optional time range.
|
||||
"""
|
||||
|
||||
# Access data from the Pydantic model
|
||||
device_id = request_data.device_id
|
||||
|
||||
# Validate 'device_id'
|
||||
# (This is already validated by Pydantic, but additional logic can be added if needed)
|
||||
if not device_id:
|
||||
raise HTTPException(status_code=400, detail="'device_id' parameter is required")
|
||||
|
||||
# Simulate reboot operation and return the response
|
||||
statistics = []
|
||||
# Placeholder for actual data retrieval or device reboot logic
|
||||
stats = {"data": f"Device {device_id} has been successfully rebooted."}
|
||||
statistics.append(stats)
|
||||
|
||||
# Return the response with a summary
|
||||
return CoverageResponse(status="success", summary={"device_id": device_id})
|
||||
|
||||
|
||||
# Post method for device summary
|
||||
@app.post("/agent/device_summary", response_model=DeviceSummaryResponse)
|
||||
def get_device_summary(request: DeviceSummaryRequest):
|
||||
"""
|
||||
Endpoint to retrieve device statistics based on device IDs and an optional time range.
|
||||
"""
|
||||
|
||||
# Extract 'device_id' and 'time_range' from the request
|
||||
device_id = request.device_id
|
||||
time_range = request.time_range
|
||||
|
||||
# Simulate retrieving statistics for the given device IDs and time range
|
||||
statistics = []
|
||||
minutes = 4
|
||||
stats = {
|
||||
"device_id": device_id,
|
||||
"time_range": f"Last {time_range} days",
|
||||
"data": f"""Device {device_id} over the last {time_range} days experienced {minutes}
|
||||
minutes of downtime.""",
|
||||
}
|
||||
|
||||
statistics.append(DeviceStatistics(**stats))
|
||||
|
||||
return DeviceSummaryResponse(statistics=statistics)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
typing
|
||||
pandas
|
||||
gradio==5.3.0
|
||||
huggingface_hub<1.0.0
|
||||
async_timeout==4.0.3
|
||||
loguru==0.7.2
|
||||
asyncio==3.4.3
|
||||
httpx==0.27.0
|
||||
python-dotenv==1.0.1
|
||||
pydantic==2.8.2
|
||||
openai==1.51.0
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Function to start the demo
|
||||
start_demo() {
|
||||
# Step 1: Check if .env file exists
|
||||
if [ -f ".env" ]; then
|
||||
echo ".env file already exists. Skipping creation."
|
||||
else
|
||||
# Step 2: Create `.env` file and set OpenAI key
|
||||
if [ -z "$OPENAI_API_KEY" ]; then
|
||||
echo "Error: OPENAI_API_KEY environment variable is not set for the demo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating .env file..."
|
||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" > .env
|
||||
echo ".env file created with OPENAI_API_KEY."
|
||||
fi
|
||||
|
||||
# Step 3: Start Arch
|
||||
echo "Starting Arch with arch_config.yaml..."
|
||||
archgw up arch_config.yaml
|
||||
|
||||
# Step 4: Start developer services
|
||||
echo "Starting Network Agent using Docker Compose..."
|
||||
docker compose up -d # Run in detached mode
|
||||
}
|
||||
|
||||
# Function to stop the demo
|
||||
stop_demo() {
|
||||
# Step 1: Stop Docker Compose services
|
||||
echo "Stopping Network Agent using Docker Compose..."
|
||||
docker compose down
|
||||
|
||||
# Step 2: Stop Arch
|
||||
echo "Stopping Arch..."
|
||||
archgw down
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
if [ "$1" == "down" ]; then
|
||||
stop_demo
|
||||
else
|
||||
# Default action is to bring the demo up
|
||||
start_demo
|
||||
fi
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
# 🗝️ RouteGPT (Beta)
|
||||
|
||||
**RouteGPT** is a dynamic model selector Chrome extension for ChatGPT. It intercepts your prompts, detects the user's intent, and automatically routes requests to the most appropriate model — based on preferences you define. Powered by the lightweight [Arch-Router](https://huggingface.co/katanemo/Arch-Router-1.5B.gguf), it makes multi-model usage seamless.
|
||||
|
||||
Think of it this way: changing models manually is like shifting gears on your bike every few pedals. RouteGPT automates that for you — so you can focus on the ride, not the mechanics.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Name
|
||||
|
||||
Folder: `chatgpt-preference-model-selector`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
* 🧠 Preference-based routing (e.g., "code generation" → GPT-4, "travel help" → Gemini)
|
||||
* 🤖 Local inference using [Ollama](https://ollama.com)
|
||||
* 📙 Chrome extension interface for setting route preferences
|
||||
* ⚡ Runs with [Arch-Router-1.5B.gguf](https://huggingface.co/katanemo/Arch-Router-1.5B.gguf)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### 1. Clone and install dependencies
|
||||
|
||||
```
|
||||
git clone https://github.com/katanemo/archgw/
|
||||
cd demos/use_cases/chatgpt-preference-model-selector
|
||||
```
|
||||
|
||||
### 2. Build the extension
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
This will create a `build/` directory that contains the unpacked Chrome extension.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Set Up Arch-Router in Ollama
|
||||
|
||||
Ensure [Ollama](https://ollama.com/download) is installed and running.
|
||||
|
||||
Then pull the Arch-Router model:
|
||||
|
||||
```
|
||||
ollama pull hf.co/katanemo/Arch-Router-1.5B.gguf:Q4_K_M
|
||||
```
|
||||
|
||||
### 🌐 Allow Chrome to Access Ollama
|
||||
|
||||
Start Ollama with appropriate network settings:
|
||||
|
||||
```
|
||||
OLLAMA_ORIGINS=* ollama serve
|
||||
```
|
||||
|
||||
This:
|
||||
* Sets CORS to allow requests from Chrome
|
||||
|
||||
---
|
||||
|
||||
## 📩 Load the Extension into Chrome
|
||||
|
||||
1. Open `chrome://extensions`
|
||||
2. Enable **Developer mode** (top-right toggle)
|
||||
3. Click **"Load unpacked"**
|
||||
4. Select the `build` folder inside `chatgpt-preference-model-selector`
|
||||
|
||||
Once loaded, RouteGPT will begin intercepting and routing your ChatGPT messages based on the preferences you define.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configure Routing Preferences
|
||||
|
||||
1. In ChatGPT, click the model dropdown.
|
||||
2. A RouteGPT modal will appear.
|
||||
3. Define your routing logic using natural language (e.g., `brainstorm startup ideas → gpt-4`, `summarize news articles → claude`).
|
||||
4. Save your preferences. Routing begins immediately.
|
||||
|
||||
---
|
||||
|
||||
## 💸 Profit
|
||||
|
||||
RouteGPT helps you:
|
||||
|
||||
* Use expensive models only when needed
|
||||
* Automatically shift to cheaper, faster, or more capable models based on task type
|
||||
* Streamline multi-model workflows without extra clicks
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Troubleshooting
|
||||
|
||||
* Make sure Ollama is reachable at `http://localhost:11434`
|
||||
* If routing doesn’t seem to trigger, check DevTools console logs for `[ModelSelector]`
|
||||
* Reload the extension and refresh the ChatGPT tab after updating preferences
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Built With
|
||||
|
||||
* 🧠 [Arch-Router (1.5B)](https://huggingface.co/katanemo/Arch-Router-1.5B.gguf)
|
||||
* 📙 Chrome Extensions API
|
||||
* 🛠️ Ollama
|
||||
* ⚛️ React + TypeScript
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
Apache 2.0 © Katanemo Labs, Inc.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"name": "preference-selector-extension",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "node src/build.js",
|
||||
"test": "react-scripts test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"react-scripts": "5.0.1",
|
||||
"tailwindcss": "^3.4.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<!-- ✅ External JS to configure Tailwind and set dark mode -->
|
||||
<script src="%PUBLIC_URL%/init-theme.js"></script>
|
||||
|
||||
<title>RouteGPT</title>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-100">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
// Apply dark mode based on system preference
|
||||
if (
|
||||
localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "RouteGPT",
|
||||
"version": "0.1.2",
|
||||
"description": "RouteGPT: Smart Model Routing for ChatGPT.",
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://chatgpt.com/*",
|
||||
"http://localhost:12000/*"
|
||||
],
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' http://localhost:12000"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["index.html", "logo.png"],
|
||||
"matches": ["https://chatgpt.com/*"]
|
||||
},
|
||||
{
|
||||
"resources": ["pageFetchOverride.js"],
|
||||
"matches": ["https://chatgpt.com/*"]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "index.html"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://chatgpt.com/*"],
|
||||
"js": ["static/js/content.js"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import React from 'react';
|
||||
import PreferenceBasedModelSelector from './components/PreferenceBasedModelSelector';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="bg-gray-100 dark:bg-gray-900 min-h-screen flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-6xl">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center items-center gap-3 -ml-12">
|
||||
<img src="/logo.png" alt="RouteGPT Logo" className="w-10 h-10" />
|
||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">RouteGPT</h1>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 mt-2">
|
||||
Dynamically route to GPT models based on usage preferences.
|
||||
</p>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/katanemo/archgw"
|
||||
className="text-blue-500 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
powered by Arch Router
|
||||
</a>
|
||||
</div>
|
||||
<PreferenceBasedModelSelector />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
|
|
@ -1,64 +0,0 @@
|
|||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('Starting the custom build process for the Chrome Extension...');
|
||||
|
||||
const reactAppDir = path.join(__dirname, '..');
|
||||
const contentScriptSource = path.join(reactAppDir, 'src', 'scripts', 'content.js');
|
||||
const pageOverrideSource = path.join(reactAppDir, 'src', 'scripts', 'pageFetchOverride.js');
|
||||
const buildDir = path.join(reactAppDir, 'build');
|
||||
const contentScriptDest = path.join(buildDir, 'static', 'js');
|
||||
|
||||
// 1️⃣ Run React build
|
||||
try {
|
||||
console.log('Running react-scripts build...');
|
||||
execSync('react-scripts build', { stdio: 'inherit' });
|
||||
console.log('React build completed successfully.');
|
||||
} catch (err) {
|
||||
console.error('React build failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2️⃣ Copy content.js
|
||||
try {
|
||||
if (!fs.existsSync(contentScriptDest)) {
|
||||
throw new Error(`Missing directory: ${contentScriptDest}`);
|
||||
}
|
||||
fs.copyFileSync(contentScriptSource, path.join(contentScriptDest, 'content.js'));
|
||||
console.log(`Copied content.js → ${contentScriptDest}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy content.js:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3️⃣ Copy pageFetchOverride.js
|
||||
try {
|
||||
if (!fs.existsSync(buildDir)) {
|
||||
throw new Error(`Missing build directory: ${buildDir}`);
|
||||
}
|
||||
fs.copyFileSync(pageOverrideSource, path.join(buildDir, 'pageFetchOverride.js'));
|
||||
console.log(`Copied pageFetchOverride.js → ${buildDir}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy pageFetchOverride.js:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 4️⃣ Copy logo.png from src/assets to build root
|
||||
try {
|
||||
const logoSource = path.join(reactAppDir, 'src', 'assets', 'logo.png');
|
||||
const logoDest = path.join(buildDir, 'logo.png');
|
||||
|
||||
if (!fs.existsSync(logoSource)) {
|
||||
throw new Error(`Missing logo.png at ${logoSource}`);
|
||||
}
|
||||
|
||||
fs.copyFileSync(logoSource, logoDest);
|
||||
console.log(`Copied logo.png → ${logoDest}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy logo.png:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
console.log('Extension build process finished successfully!');
|
||||
|
|
@ -1,327 +0,0 @@
|
|||
/*global chrome*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
// --- Hard‑coded list of ChatGPT models ---
|
||||
const MODEL_LIST = [
|
||||
'gpt-4o',
|
||||
'gpt-4.1',
|
||||
'gpt-4.1-mini',
|
||||
'gpt-4.5-preview',
|
||||
'o3',
|
||||
'o4-mini',
|
||||
'o4-mini-high'
|
||||
];
|
||||
|
||||
// --- Mocked lucide-react icons as SVG components ---
|
||||
const Trash2 = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
<line x1="10" y1="11" x2="10" y2="17" />
|
||||
<line x1="14" y1="11" x2="14" y2="17" />
|
||||
</svg>
|
||||
);
|
||||
const PlusCircle = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="8" y1="12" x2="16" y2="12" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// --- Mocked UI Components ---
|
||||
const Card = ({ children, className = '' }) => (
|
||||
<div className={`bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const CardContent = ({ children, className = '' }) => (
|
||||
<div className={`p-4 text-gray-800 dark:text-gray-100 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Input = (props) => (
|
||||
<input
|
||||
{...props}
|
||||
className={`w-full h-9 px-3 text-sm
|
||||
text-gray-800 dark:text-white
|
||||
bg-white dark:bg-gray-700
|
||||
border border-gray-300 dark:border-gray-600
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
${props.className || ''}`}
|
||||
/>
|
||||
);
|
||||
|
||||
const Button = ({ children, variant = 'default', size = 'default', className = '', ...props }) => {
|
||||
const baseClasses = `
|
||||
inline-flex items-center justify-center
|
||||
rounded-md text-sm font-medium
|
||||
transition-colors
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2
|
||||
`;
|
||||
|
||||
const variantClasses = {
|
||||
default: `
|
||||
bg-gray-900 text-white
|
||||
hover:bg-gray-800
|
||||
focus:ring-gray-900
|
||||
`,
|
||||
outline: `
|
||||
border border-gray-300 dark:border-gray-600
|
||||
bg-transparent
|
||||
text-gray-800 dark:text-white
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700
|
||||
focus:ring-blue-500
|
||||
focus:ring-offset-2
|
||||
dark:focus:ring-offset-gray-900
|
||||
`,
|
||||
ghost: `
|
||||
text-gray-800 dark:text-gray-200
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700
|
||||
focus:ring-gray-400
|
||||
`
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
default: 'h-9 px-3',
|
||||
icon: 'h-9 w-9'
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={`
|
||||
${baseClasses}
|
||||
${variantClasses[variant]}
|
||||
${sizeClasses[size]}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const Switch = ({ checked, onCheckedChange, id }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => onCheckedChange(!checked)}
|
||||
id={id}
|
||||
className={`
|
||||
relative inline-flex items-center justify-start
|
||||
h-6 w-11 rounded-full
|
||||
transition-colors duration-200 ease-in-out
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||
border-2 border-transparent
|
||||
overflow-hidden
|
||||
${checked ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`
|
||||
pointer-events-none
|
||||
inline-block h-5 w-5 transform rounded-full bg-white
|
||||
shadow-md ring-0 transition-transform duration-200 ease-in-out
|
||||
${checked ? 'translate-x-[20px]' : 'translate-x-0'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
<span className="inline-block w-8 text-sm text-gray-700 dark:text-gray-300 text-center select-none">
|
||||
{checked ? 'On' : 'Off'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Label = (props) => (
|
||||
<label {...props} className={`text-sm font-medium leading-none text-gray-700 ${props.className || ''}`} />
|
||||
);
|
||||
|
||||
export default function PreferenceBasedModelSelector() {
|
||||
const [routingEnabled, setRoutingEnabled] = useState(false);
|
||||
const [preferences, setPreferences] = useState([
|
||||
{ id: 1, usage: 'generate code snippets', model: 'gpt-4o' }
|
||||
]);
|
||||
const [defaultModel, setDefaultModel] = useState('gpt-4o');
|
||||
const [modelOptions] = useState(MODEL_LIST); // static list, no dynamic fetch
|
||||
|
||||
// Load saved settings
|
||||
useEffect(() => {
|
||||
chrome.storage.sync.get(['routingEnabled', 'preferences', 'defaultModel'], (result) => {
|
||||
if (result.routingEnabled !== undefined) setRoutingEnabled(result.routingEnabled);
|
||||
|
||||
if (result.preferences) {
|
||||
// add ids if they were missing
|
||||
const withIds = result.preferences.map((p, i) => ({
|
||||
id: p.id ?? i + 1,
|
||||
...p,
|
||||
}));
|
||||
setPreferences(withIds);
|
||||
}
|
||||
|
||||
if (result.defaultModel) setDefaultModel(result.defaultModel);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updatePreference = (id, key, value) => {
|
||||
setPreferences((prev) => prev.map((p) => (p.id === id ? { ...p, [key]: value } : p)));
|
||||
};
|
||||
|
||||
const addPreference = () => {
|
||||
const newId = preferences.reduce((max, p) => Math.max(max, p.id ?? 0), 0) + 1;
|
||||
setPreferences((prev) => [
|
||||
...prev,
|
||||
{ id: newId, usage: '', model: defaultModel }
|
||||
]);
|
||||
};
|
||||
|
||||
const removePreference = (id) => {
|
||||
if (preferences.length > 1) {
|
||||
setPreferences((prev) => prev.filter((p) => p.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
// Save settings: generate name slug and store tuples
|
||||
const handleSave = () => {
|
||||
const slugCounts = {};
|
||||
const tuples = [];
|
||||
|
||||
preferences
|
||||
.filter(p => p.usage?.trim())
|
||||
.forEach(p => {
|
||||
const baseSlug = p.usage
|
||||
.split(/\s+/)
|
||||
.slice(0, 3)
|
||||
.join('-')
|
||||
.toLowerCase()
|
||||
.replace(/[^\w-]/g, '');
|
||||
|
||||
const count = slugCounts[baseSlug] || 0;
|
||||
slugCounts[baseSlug] = count + 1;
|
||||
|
||||
const dedupedSlug = count === 0 ? baseSlug : `${baseSlug}-${count}`;
|
||||
|
||||
tuples.push({
|
||||
name: dedupedSlug,
|
||||
usage: p.usage.trim(),
|
||||
model: p.model?.trim?.() || ''
|
||||
});
|
||||
});
|
||||
|
||||
chrome.storage.sync.set({ routingEnabled, preferences: tuples, defaultModel }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('[PBMS] Storage error:', chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log('[PBMS] Saved tuples:', tuples);
|
||||
}
|
||||
});
|
||||
// Send message to background script to apply the default model
|
||||
window.parent.postMessage({ action: 'applyModelSelection', model: defaultModel }, "*");
|
||||
|
||||
// Close the modal after saving
|
||||
window.parent.postMessage({ action: 'CLOSE_PBMS_MODAL' }, '*');
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
window.parent.postMessage({ action: 'CLOSE_PBMS_MODAL' }, '*');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[600px] h-[65vh] flex flex-col bg-gray-50 dark:bg-gray-800 p-4 mx-auto">
|
||||
|
||||
{/* Scrollable preferences only */}
|
||||
<div className="space-y-4 overflow-y-auto flex-1 pr-1 max-h-[60vh]">
|
||||
<Card className="w-full">
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Enable preference-based routing</Label>
|
||||
<Switch checked={routingEnabled} onCheckedChange={setRoutingEnabled} />
|
||||
</div>
|
||||
{routingEnabled && (
|
||||
<div className="pt-4 mt-4 space-y-3 border-t border-gray-200 dark:border-gray-700">
|
||||
{preferences.map((pref) => (
|
||||
<div key={pref.id} className="grid grid-cols-[3fr_1.5fr_auto] gap-4 items-center">
|
||||
<Input
|
||||
placeholder="(e.g. generating fictional stories or poems)"
|
||||
value={pref.usage}
|
||||
onChange={(e) => updatePreference(pref.id, 'usage', e.target.value)}
|
||||
/>
|
||||
<select
|
||||
value={pref.model}
|
||||
onChange={(e) => updatePreference(pref.id, 'model', e.target.value)}
|
||||
className="h-9 w-full px-3 text-sm
|
||||
bg-white dark:bg-gray-700
|
||||
text-gray-800 dark:text-white
|
||||
border border-gray-300 dark:border-gray-600
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option disabled value="">
|
||||
Select Model
|
||||
</option>
|
||||
{modelOptions.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removePreference(pref.id)}
|
||||
disabled={preferences.length <= 1}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-gray-500 hover:text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={addPreference}
|
||||
className="flex items-center gap-2 text-sm mt-2"
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" /> Add Preference
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Default model selector (static) */}
|
||||
<Card className="w-full mt-4">
|
||||
<CardContent>
|
||||
<Label>Default Model</Label>
|
||||
<select
|
||||
value={defaultModel}
|
||||
onChange={(e) => setDefaultModel(e.target.value)}
|
||||
className="h-9 w-full mt-2 px-3 text-sm
|
||||
bg-white dark:bg-gray-700
|
||||
text-gray-800 dark:text-white
|
||||
border border-gray-300 dark:border-gray-600
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{modelOptions.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Save/Cancel footer (static) */}
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 mt-4">
|
||||
<Button variant="ghost" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>Save and Apply</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
@ -1,407 +0,0 @@
|
|||
(() => {
|
||||
const TAG = '[ModelSelector]';
|
||||
// Content script to intercept fetch requests and modify them based on user preferences
|
||||
async function streamToPort(response, port) {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
port.postMessage({ done: true });
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
port.postMessage({ done: true });
|
||||
break;
|
||||
}
|
||||
port.postMessage({ chunk: value.buffer }, [value.buffer]);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract messages from the DOM, falling back to requestMessages if DOM is empty
|
||||
function getMessagesFromDom(requestMessages = null) {
|
||||
const bubbles = [...document.querySelectorAll('[data-message-author-role]')];
|
||||
|
||||
const domMessages = bubbles
|
||||
.map(b => {
|
||||
const role = b.getAttribute('data-message-author-role');
|
||||
const content =
|
||||
role === 'assistant'
|
||||
? (b.querySelector('.markdown')?.innerText ?? b.innerText ?? '').trim()
|
||||
: (b.innerText ?? '').trim();
|
||||
return content ? { role, content } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// Fallback: If DOM is empty but we have requestMessages, use those
|
||||
if (domMessages.length === 0 && requestMessages?.length > 0) {
|
||||
return requestMessages
|
||||
.map(msg => {
|
||||
const role = msg.author?.role;
|
||||
const parts = msg.content?.parts ?? [];
|
||||
const textPart = parts.find(p => typeof p === 'string');
|
||||
return role && textPart ? { role, content: textPart.trim() } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return domMessages;
|
||||
}
|
||||
|
||||
// Insert a route label for the last user message in the chat
|
||||
function insertRouteLabelForLastUserMessage(routeName) {
|
||||
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
|
||||
// Find the most recent user bubble
|
||||
const bubbles = [...document.querySelectorAll('[data-message-author-role="user"]')];
|
||||
const lastBubble = bubbles[bubbles.length - 1];
|
||||
if (!lastBubble) return;
|
||||
|
||||
// Skip if we’ve already added a label
|
||||
if (lastBubble.querySelector('.arch-route-label')) {
|
||||
console.log('[RouteLabel] Label already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Default label text
|
||||
let labelText = 'RouteGPT: preference = default';
|
||||
|
||||
// Try to override with preference-based usage if we have a routeName
|
||||
if (routeName && Array.isArray(preferences)) {
|
||||
const match = preferences.find(p => p.name === routeName);
|
||||
if (match && match.usage) {
|
||||
labelText = `RouteGPT: preference = ${match.usage}`;
|
||||
} else {
|
||||
console.log('[RouteLabel] No usage found for route (falling back to default):', routeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Build and attach the label
|
||||
const label = document.createElement('span');
|
||||
label.textContent = labelText;
|
||||
label.className = 'arch-route-label';
|
||||
label.style.fontWeight = '350';
|
||||
label.style.fontSize = '0.85rem';
|
||||
label.style.marginTop = '2px';
|
||||
label.style.fontStyle = 'italic';
|
||||
label.style.alignSelf = 'end';
|
||||
label.style.marginRight = '5px';
|
||||
|
||||
lastBubble.appendChild(label);
|
||||
console.log('[RouteLabel] Inserted label:', labelText);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Prepare the system prompt for the proxy request
|
||||
function prepareProxyRequest(messages, routes, maxTokenLength = 2048) {
|
||||
const SYSTEM_PROMPT_TEMPLATE = `
|
||||
You are a helpful assistant designed to find the best suited route.
|
||||
You are provided with route description within <routes></routes> XML tags:
|
||||
<routes>
|
||||
{routes}
|
||||
</routes>
|
||||
|
||||
<conversation>
|
||||
{conversation}
|
||||
</conversation>
|
||||
|
||||
Your task is to decide which route is best suit with user intent on the conversation in <conversation></conversation> XML tags. Follow the instruction:
|
||||
1. If the latest intent from user is irrelevant or user intent is full filled, response with other route {"route": "other"}.
|
||||
2. You must analyze the route descriptions and find the best match route for user latest intent.
|
||||
3. You only response the name of the route that best matches the user's request, use the exact name in the <routes></routes>.
|
||||
|
||||
Based on your analysis, provide your response in the following JSON formats if you decide to match any route:
|
||||
{"route": "route_name"}
|
||||
`;
|
||||
const TOKEN_DIVISOR = 4;
|
||||
|
||||
const filteredMessages = messages.filter(
|
||||
m => m.role !== 'system' && m.role !== 'tool' && m.content?.trim()
|
||||
);
|
||||
|
||||
let tokenCount = SYSTEM_PROMPT_TEMPLATE.length / TOKEN_DIVISOR;
|
||||
const selected = [];
|
||||
|
||||
for (let i = filteredMessages.length - 1; i >= 0; i--) {
|
||||
const msg = filteredMessages[i];
|
||||
tokenCount += msg.content.length / TOKEN_DIVISOR;
|
||||
|
||||
if (tokenCount > maxTokenLength) {
|
||||
if (msg.role === 'user') selected.push(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
selected.push(msg);
|
||||
}
|
||||
|
||||
if (selected.length === 0 && filteredMessages.length > 0) {
|
||||
selected.push(filteredMessages[filteredMessages.length - 1]);
|
||||
}
|
||||
|
||||
const selectedOrdered = selected.reverse();
|
||||
|
||||
const systemPrompt = SYSTEM_PROMPT_TEMPLATE
|
||||
.replace('{routes}', JSON.stringify(routes, null, 2))
|
||||
.replace('{conversation}', JSON.stringify(selectedOrdered, null, 2));
|
||||
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
function getRoutesFromStorage() {
|
||||
return new Promise(resolve => {
|
||||
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
|
||||
if (!preferences || !Array.isArray(preferences)) {
|
||||
console.warn('[ModelSelector] No preferences found in storage');
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
const routes = preferences.map(p => ({
|
||||
name: p.name,
|
||||
description: p.usage
|
||||
}));
|
||||
|
||||
resolve(routes);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getModelIdForRoute(routeName) {
|
||||
return new Promise(resolve => {
|
||||
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
|
||||
const match = (preferences || []).find(p => p.name === routeName);
|
||||
if (match) resolve(match.model);
|
||||
else resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(function injectPageFetchOverride() {
|
||||
const injectorTag = '[ModelSelector][Injector]';
|
||||
const s = document.createElement('script');
|
||||
s.src = chrome.runtime.getURL('pageFetchOverride.js');
|
||||
s.onload = () => {
|
||||
console.log(`${injectorTag} loaded pageFetchOverride.js`);
|
||||
s.remove();
|
||||
};
|
||||
(document.head || document.documentElement).appendChild(s);
|
||||
})();
|
||||
|
||||
window.addEventListener('message', ev => {
|
||||
if (ev.source !== window || ev.data?.type !== 'ARCHGW_FETCH') return;
|
||||
|
||||
const { url, init } = ev.data;
|
||||
const port = ev.ports[0];
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log(`${TAG} Intercepted fetch from page:`, url);
|
||||
|
||||
let originalBody = {};
|
||||
try {
|
||||
originalBody = JSON.parse(init.body);
|
||||
} catch {
|
||||
console.warn(`${TAG} Could not parse original fetch body`);
|
||||
}
|
||||
|
||||
const { routingEnabled, preferences, defaultModel } = await new Promise(resolve => {
|
||||
chrome.storage.sync.get(['routingEnabled', 'preferences', 'defaultModel'], resolve);
|
||||
});
|
||||
|
||||
if (!routingEnabled) {
|
||||
console.log(`${TAG} Routing disabled — applying default model if present`);
|
||||
const modifiedBody = { ...originalBody };
|
||||
if (defaultModel) {
|
||||
modifiedBody.model = defaultModel;
|
||||
console.log(`${TAG} Routing disabled — overriding with default model: ${defaultModel}`);
|
||||
} else {
|
||||
console.log(`${TAG} Routing disabled — no default model found`);
|
||||
}
|
||||
|
||||
await streamToPort(await fetch(url, {
|
||||
method: init.method,
|
||||
headers: init.headers,
|
||||
credentials: init.credentials,
|
||||
body: JSON.stringify(modifiedBody)
|
||||
}), port);
|
||||
return;
|
||||
}
|
||||
|
||||
const scrapedMessages = getMessagesFromDom(originalBody.messages);
|
||||
const routes = (preferences || []).map(p => ({
|
||||
name: p.name,
|
||||
description: p.usage
|
||||
}));
|
||||
const prompt = prepareProxyRequest(scrapedMessages, routes);
|
||||
|
||||
let selectedRoute = null;
|
||||
try {
|
||||
const res = await fetch('http://localhost:11434/api/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'hf.co/katanemo/Arch-Router-1.5B.gguf:Q4_K_M',
|
||||
prompt: prompt,
|
||||
temperature: 0.01,
|
||||
top_p: 0.95,
|
||||
top_k: 20,
|
||||
stream: false
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
console.log(`${TAG} Ollama router response:`, data.response);
|
||||
try {
|
||||
let parsed = data.response;
|
||||
if (typeof data.response === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(data.response);
|
||||
} catch (jsonErr) {
|
||||
const safe = data.response.replace(/'/g, '"');
|
||||
parsed = JSON.parse(safe);
|
||||
}
|
||||
}
|
||||
selectedRoute = parsed.route || null;
|
||||
if (!selectedRoute) console.warn(`${TAG} Route missing in parsed response`);
|
||||
} catch (e) {
|
||||
console.warn(`${TAG} Failed to parse or extract route from response`, e);
|
||||
}
|
||||
} else {
|
||||
console.warn(`${TAG} Ollama router failed:`, res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`${TAG} Ollama request error`, err);
|
||||
}
|
||||
|
||||
let targetModel = null;
|
||||
if (selectedRoute) {
|
||||
targetModel = await getModelIdForRoute(selectedRoute);
|
||||
if (!targetModel) {
|
||||
const { defaultModel } = await new Promise(resolve =>
|
||||
chrome.storage.sync.get(['defaultModel'], resolve)
|
||||
);
|
||||
targetModel = defaultModel || null;
|
||||
if (targetModel) {
|
||||
console.log(`${TAG} Falling back to default model: ${targetModel}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`${TAG} Resolved model for route "${selectedRoute}" →`, targetModel);
|
||||
}
|
||||
}
|
||||
|
||||
insertRouteLabelForLastUserMessage(selectedRoute);
|
||||
const modifiedBody = { ...originalBody };
|
||||
if (targetModel) {
|
||||
modifiedBody.model = targetModel;
|
||||
console.log(`${TAG} Overriding request with model: ${targetModel}`);
|
||||
} else {
|
||||
console.log(`${TAG} No route/model override applied`);
|
||||
}
|
||||
|
||||
await streamToPort(await fetch(url, {
|
||||
method: init.method,
|
||||
headers: init.headers,
|
||||
credentials: init.credentials,
|
||||
body: JSON.stringify(modifiedBody)
|
||||
}), port);
|
||||
} catch (err) {
|
||||
console.error(`${TAG} Proxy fetch error`, err);
|
||||
port.postMessage({ done: true });
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
let desiredModel = null;
|
||||
|
||||
function patchDom() {
|
||||
if (!desiredModel) return;
|
||||
|
||||
const btn = document.querySelector('[data-testid="model-switcher-dropdown-button"]');
|
||||
if (!btn) return;
|
||||
|
||||
const span = btn.querySelector('div > span');
|
||||
const wantLabel = `Model selector, current model is ${desiredModel}`;
|
||||
|
||||
if (span && span.textContent !== desiredModel) {
|
||||
span.textContent = desiredModel;
|
||||
}
|
||||
|
||||
if (btn.getAttribute('aria-label') !== wantLabel) {
|
||||
btn.setAttribute('aria-label', wantLabel);
|
||||
}
|
||||
}
|
||||
|
||||
// Observe DOM mutations and reactively patch
|
||||
const observer = new MutationObserver(patchDom);
|
||||
observer.observe(document.body || document.documentElement, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
// Set initial model from storage (optional default)
|
||||
chrome.storage.sync.get(['defaultModel'], ({ defaultModel }) => {
|
||||
if (defaultModel) {
|
||||
desiredModel = defaultModel;
|
||||
patchDom();
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Only listen for messages from iframe via window.postMessage
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
if (
|
||||
typeof data === 'object' &&
|
||||
data?.action === 'applyModelSelection' &&
|
||||
typeof data.model === 'string'
|
||||
) {
|
||||
|
||||
desiredModel = data.model;
|
||||
patchDom();
|
||||
}
|
||||
});
|
||||
|
||||
function showModal() {
|
||||
if (document.getElementById('pbms-overlay')) return;
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'pbms-overlay';
|
||||
Object.assign(overlay.style, {
|
||||
position: 'fixed', top: 0, left: 0,
|
||||
width: '100vw', height: '100vh',
|
||||
background: 'rgba(0,0,0,0.4)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
zIndex: 2147483647
|
||||
});
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = chrome.runtime.getURL('index.html');
|
||||
Object.assign(iframe.style, {
|
||||
width: '500px', height: '600px',
|
||||
border: 0, borderRadius: '8px',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
|
||||
background: 'white', zIndex: 2147483648
|
||||
});
|
||||
overlay.addEventListener('click', e => e.target === overlay && overlay.remove());
|
||||
overlay.appendChild(iframe);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function interceptDropdown(ev) {
|
||||
const btn = ev.target.closest('button[data-testid="model-switcher-dropdown-button"]');
|
||||
if (!btn) return;
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
showModal();
|
||||
}
|
||||
|
||||
document.addEventListener('pointerdown', interceptDropdown, true);
|
||||
document.addEventListener('mousedown', interceptDropdown, true);
|
||||
|
||||
window.addEventListener('message', ev => {
|
||||
if (ev.data?.action === 'CLOSE_PBMS_MODAL') {
|
||||
document.getElementById('pbms-overlay')?.remove();
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`${TAG} content script initialized`);
|
||||
})();
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
(function() {
|
||||
const TAG = '[ModelSelector][Page]';
|
||||
console.log(`${TAG} installing fetch override`);
|
||||
|
||||
const origFetch = window.fetch;
|
||||
window.fetch = async function(input, init = {}) {
|
||||
|
||||
const urlString = typeof input === 'string' ? input : input.url;
|
||||
const urlObj = new URL(urlString, window.location.origin);
|
||||
const pathname = urlObj.pathname;
|
||||
console.log(`${TAG} fetch →`, pathname);
|
||||
|
||||
const method = (init.method || 'GET').toUpperCase();
|
||||
if (method === 'OPTIONS') {
|
||||
console.log(`${TAG} OPTIONS request → bypassing completely`);
|
||||
return origFetch(input, init);
|
||||
}
|
||||
|
||||
// Only intercept conversation fetches
|
||||
if (pathname === '/backend-api/conversation' || pathname === '/backend-api/f/conversation') {
|
||||
console.log(`${TAG} matched → proxy via content script`);
|
||||
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
|
||||
// ✅ Remove non-cloneable properties like 'signal'
|
||||
const safeInit = { ...init };
|
||||
delete safeInit.signal;
|
||||
|
||||
// Forward the fetch details to the content script
|
||||
window.postMessage({
|
||||
type: 'ARCHGW_FETCH',
|
||||
url: urlString,
|
||||
init: safeInit
|
||||
}, '*', [port2]);
|
||||
|
||||
// Return a stream response that the content script will fulfill
|
||||
return new Response(new ReadableStream({
|
||||
start(controller) {
|
||||
port1.onmessage = ({ data }) => {
|
||||
if (data.done) {
|
||||
controller.close();
|
||||
port1.close();
|
||||
} else {
|
||||
controller.enqueue(new Uint8Array(data.chunk));
|
||||
}
|
||||
};
|
||||
},
|
||||
cancel() {
|
||||
port1.close();
|
||||
}
|
||||
}), {
|
||||
headers: { 'Content-Type': 'text/event-stream' }
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, pass through to the original fetch
|
||||
return origFetch(input, init);
|
||||
};
|
||||
|
||||
console.log(`${TAG} fetch override installed`);
|
||||
})();
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: 'class', // ✅ Add this line
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
"./public/index.html",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue