브라우저에서 완전히 실행되는 멀티 세션 AI 멀티플렉서
요약
브라우저 환경에서 완전히 실행되는 파일 기반 AI 도구인 OllamaTerminal을 소개합니다. 이 도구는 채팅, 명령 실행, 그리고 localhost에서 실행 중인 MCP 서버와의 연동을 지원하는 멀티 세션 AI 멀티플렉서입니다.
핵심 포인트
- 브라우저 기반의 파일 기반 AI 도구 구현
- MCP(Model Context Protocol) 서버 연동 지원
- 멀티 세션 AI 멀티플렉싱 기능 제공
- Dracula 테마를 적용한 터미널 스타일 UI
여기에 채팅을 하고, 주어진 명령을 실행하며, localhost:3000에서 실행 중인 MCP 서버를 사용하는, 그리 단순하지 않은 파일 기반 AI 도구가 있습니다:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OllamaTerminal — Dracula</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Fira+Code:wght@300;400;500;600&display=swap" rel="stylesheet" />
<style>
/* ─── DRACULA PALETTE ─────────────────────────────────────────── */
:root {
--bg: #282a36;
--bg-dark: #1e1f29;
--bg-darker: #191a24;
--surface: #21222c;
--surface2: #2d2f3d;
--border: #44475a;
--comment: #6272a4;
--fg: #f8f8f2;
--fg-dim: #c8c8c0;
--cyan: #8be9fd;
--green: #50fa7b;
--orange: #ffb86c;
--pink: #ff79c6;
--purple: #bd93f9;
--red: #ff5555;
--yellow: #f1fa8c;
--accent: #bd93f9;
--accent-glow: rgba(189,147,249,0.25);
--green-glow: rgba(80,250,123,0.2);
--cyan-glow: rgba(139,233,253,0.2);
--radius: 4px;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg-darker);
color: var(--fg);
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.55;
overflow: hidden;
}
/* ─── SCANLINE OVERLAY ────────────────────────────────────────── */
body::before {
content: '';
position: fixed; inset: 0; z-index: 9999;
pointer-events: none;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.07) 2px,
rgba(0,0,0,0.07) 4px
);
}
/* ─── CHROME / WINDOW ─────────────────────────────────────────── */
#app {
display: flex;
flex-direction: column;
height: 100vh;
border: 1px solid var(--border);
background: var(--bg);
position: relative;
}
/* ─── TITLE BAR ───────────────────────────────────────────────── */
#titleBar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
background: var(--bg-darker);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
user-select: none;
}
.traffic-lights { display: flex; gap: 6px; align-items: center; }
.tl { width: 12px; height: 12px; border-radius: 50%; cursor: pointer; transition: filter .15s; }
.tl:hover { filter: brightness(1.3); }
.tl-red { background: var(--red); }
.tl-yellow { background: var(--yellow); }
.tl-green { background: var(--green); }
#titleText {
flex: 1;
text-align: center;
font-size: 11px;
font-weight: 500;
color: var(--comment);
letter-spacing: 0.12em;
text-transform: uppercase;
}
.title-badge {
font-size: 10px;
padding: 1px 7px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--purple);
letter-spacing: 0.08em;
}
/* ─── SESSION / MULTIPLEXER BAR ───────────────────────────────── */
#sessionBar {
display: flex;
align-items: stretch;
gap: 2px;
padding: 4px 8px 0;
background: var(--bg-darker);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
min-height: 34px;
flex-wrap: wrap;
}
.session-wrapper {
display: flex;
align-items: center;
position: relative;
}
.session-tab {
display: inline-flex;
align-items: center;
padding: 4px 12px;
font-size: 11.5px;
font-weight: 500;
cursor: pointer;
color: var(--comment);
border: 1px solid transparent;
border-bottom: none;
border-radius: var(--radius) var(--radius) 0 0;
background: transparent;
transition: color .15s, background .15s, border-color .15s;
white-space: nowrap;
letter-spacing: 0.03em;
position: relative;
top: 1px;
}
.session-tab:hover {
color: var(--fg-dim);
background: var(--surface);
border-color: var(--border);
}
.session-tab.active {
color: var(--purple);
background: var(--bg);
border-color: var(--border);
border-bottom-color: var(--bg);
font-weight: 600;
}
.session-tab.active::before {
content: '▸ ';
color: var(--pink);
font-size: 10px;
}
.delete-btn {
font-size: 9px;
color: var(--comment);
cursor: pointer;
padding: 0 4px;
transition: color .15s;
opacity: 0.5;
}
.session-wrapper:hover .delete-btn { opacity: 1; }
.delete-btn:hover { color: var(--red); }
.rename-input {
background: var(--surface2);
border: 1px solid var(--purple);
border-radius: var(--radius);
color: var(--fg);
font-family: var(--font-mono);
font-size: 11.5px;
padding: 2px 8px;
outline: none;
width: 110px;
box-shadow: 0 0 6px var(--accent-glow);
}
/* ─── BAR ACTIONS (right-side controls) ──────────────────────── */
.bar-actions-group {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
padding: 0 4px 4px;
}
.model-selector-wrapper {
display: flex;
align-items: center;
gap: 5px;
font-size: 10.5px;
color: var(--comment);
}
.model-select-native {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--cyan);
font-family: var(--font-mono);
font-size: 10.5px;
padding: 2px 22px 2px 7px;
cursor: pointer;
outline: none;
transition: border-color .15s;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%236272a4' d='M0 0l5 6 5-6z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 6px center;
}
.model-select-native:hover,
.model-select-native:focus { border-color: var(--purple); }
.model-select-native option { background: var(--surface); }
.bar-btn {
font-family: var(--font-mono);
font-size: 10.5px;
padding: 3px 9px;
border-radius: var(--radius);
border: 1px solid;
cursor: pointer;
transition: background .15s, color .15s, box-shadow .15s;
letter-spacing: 0.04em;
font-weight: 500;
}
.new-session-btn {
color: var(--green);
border-color: var(--green);
background: transparent;
}
.new-session-btn:hover {
background: rgba(80,250,123,0.12);
box-shadow: 0 0 8px var(--green-glow);
}
.wipe-workspace-btn {
color: var(--red);
border-color: var(--red);
background: transparent;
}
.wipe-workspace-btn:hover {
background: rgba(255,85,85,0.12);
box-shadow: 0 0 8px rgba(255,85,85,0.3);
}
/* ─── STATUS BAR ──────────────────────────────────────────────── */
#statusBar {
display: flex;
align-items: center;
gap: 14px;
padding: 3px 14px;
background: var(--bg-darker);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
font-size: 10px;
color: var(--comment);
letter-spacing: 0.04em;
}
.status-dot {
width: 6px; height: 6px;
border-radius: 50%;
display: inline-block;
margin-right: 4px;
}
.status-dot.online { background: var(--green); box-shadow: 0 0 5px var(--green); animation: pulse-green 2s infinite; }
.status-dot.offline { background: var(--red); }
@keyframes pulse-green {
0%,100% { opacity: 1; }
50% { opacity: 0.4; }
}
.status-item { display: flex; align-items: center; }
.status-sep { color: var(--border); }
#mcpStatus { color: var(--green); }
#mcpStatus.offline { color: var(--red); }
/* ─── LOG / OUTPUT AREA ───────────────────────────────────────── */
#log {
flex: 1;
overflow-y: auto;
padding: 14px 18px;
background: var(--bg);
scroll-behavior: smooth;
}
#log::-webkit-scrollbar { width: 6px; }
#log::-webkit-scrollbar-track { background: transparent; }
#log::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
#log::-webkit-scrollbar-thumb:hover { background: var(--comment); }
/* ─── LOG LINE TYPES ──────────────────────────────────────────── */
.line {
display: block;
padding: 1.5px 0;
white-space: pre-wrap;
word-break: break-word;
animation: fadein .12s ease-out;
}
@keyframes fadein {
from { opacity: 0; transform: translateY(2px); }
to { opacity: 1; transform: none; }
}
.line.user {
color: var(--green);
font-weight: 500;
}
.line.ai {
color: var(--fg);
padding-left: 0;
position: relative;
}
.line.ai span {
color: var(--purple);
font-weight: 600;
}
.line.sys {
color: var(--comment);
font-size: 11.5px;
font-style: italic;
}
.line.err {
color: var(--red);
font-weight: 600;
}
.line.tool {
color: var(--orange);
border-left: 2px solid var(--orange);
padding-left: 10px;
margin: 2px 0;
background: rgba(255,184,108,0.04);
border-radius: 0 var(--radius) var(--radius) 0;
}
/* welcome banner */
.banner-line {
color: var(--pink);
font-size: 11px;
letter-spacing: 0.08em;
}
/* ─── 구분선 (DIVIDER) ────────────────────────────────────────────── */
.log-divider {
border: none;
border-top: 1px solid var(--surface2);
margin: 8px 0;
}
/* ─── 입력 행 (INPUT ROW) ───────────────────────────────────────── */
#inputRow {
display: flex;
align-items: center;
gap: 0;
padding: 8px 14px 10px;
background: var(--bg-dark);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
#terminalPrompt {
color: var(--green);
font-weight: 600;
font-size: 13px;
white-space: nowrap;
margin-right: 8px;
flex-shrink: 0;
text-shadow: 0 0 8px var(--green-glow);
}
#input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--fg);
font-family: var(--font-mono);
font-size: 13px;
caret-color: var(--purple);
}
#input::placeholder { color: var(--border); }
.input-cursor-hint {
width: 8px; height: 15px;
background: var(--purple);
animation: blink .9s step-start infinite;
border-radius: 1px;
flex-shrink: 0;
opacity: 0.7;
}
@keyframes blink {
50% { opacity: 0; }
}
/* ─── 생각 표시기 (THINKING INDICATOR) ──────────────────────────── */
.thinking-dots {
display: inline-flex;
gap: 3px;
margin-left: 6px;
vertical-align: middle;
}
.thinking-dots span {
width: 4px; height: 4px;
background: var(--purple);
border-radius: 50%;
animation: bounce .8s ease-in-out infinite;
}
.thinking-dots span:nth-child(2) { animation-delay: .15s; }
.thinking-dots span:nth-child(3) { animation-delay: .30s; }
@keyframes bounce {
0%,100% { transform: translateY(0); opacity: .4; }
50% { transform: translateY(-4px); opacity: 1; }
}
/* ─── 스크롤바 (SCROLLBAR) ──────────────────────────────────────── */
- { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
/* ─── 글로우 포커스 입력 행 (GLOW FOCUS INPUT ROW) ──────────────── */
#inputRow:focus-within {
border-top-color: var(--purple);
box-shadow: 0 -1px 0 0 var(--accent-glow);
}
/* ─── STOP BUTTON ─────────────────────────────────────────────── */
#stopBtn {
display: none;
align-items: center;
gap: 5px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: var(--radius);
border: 1px solid var(--red);
color: var(--red);
background: rgba(255,85,85,0.08);
cursor: pointer;
letter-spacing: 0.06em;
transition: background .15s, box-shadow .15s;
flex-shrink: 0;
animation: fadein .15s ease-out;
}
#stopBtn:hover {
background: rgba(255,85,85,0.18);
box-shadow: 0 0 10px rgba(255,85,85,0.35);
}
#stopBtn.visible { display: inline-flex; }
.stop-icon {
width: 8px; height: 8px;
background: var(--red);
border-radius: 1px;
flex-shrink: 0;
}
/* ─── COLLAPSIBLE TOOL BLOCK ──────────────────────────────────── */
.tool-block {
margin: 4px 0;
border-left: 2px solid var(--orange);
border-radius: 0 var(--radius) var(--radius) 0;
background: rgba(255,184,108,0.04);
overflow: hidden;
animation: fadein .12s ease-out;
}
.tool-block-header {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
cursor: pointer;
color: var(--orange);
font-size: 12px;
font-weight: 500;
user-select: none;
transition: background .12s;
}
.tool-block-header:hover { background: rgba(255,184,108,0.08); }
.tool-chevron {
font-size: 9px;
transition: transform .2s;
opacity: 0.7;
flex-shrink: 0;
}
.tool-block.open .tool-chevron { transform: rotate(90deg); }
.tool-block-label { flex: 1; }
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기