본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 27. 21:19

브라우저에서 완전히 실행되는 멀티 세션 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가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0