|
105 | 105 | padding: 20px 24px; |
106 | 106 | contain: content; |
107 | 107 | scroll-behavior: smooth; |
| 108 | + position: relative; |
108 | 109 | } |
109 | 110 |
|
110 | 111 | /* ── Message Bubbles ── */ |
|
869 | 870 | letter-spacing: 0.3px; |
870 | 871 | } |
871 | 872 |
|
| 873 | +/* ── File Attachments ── */ |
| 874 | +#attach-btn { |
| 875 | + background: transparent; border: 1px solid var(--border-subtle); border-radius: 8px; |
| 876 | + color: var(--text-tertiary); cursor: pointer; font-size: 16px; line-height: 1; |
| 877 | + padding: 6px 8px; transition: all 0.15s; flex-shrink: 0; |
| 878 | + display: flex; align-items: center; justify-content: center; |
| 879 | +} |
| 880 | +#attach-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-subtle); } |
| 881 | +#attach-btn:active { transform: scale(0.95); } |
| 882 | +#attach-btn svg { width: 16px; height: 16px; } |
| 883 | + |
| 884 | +#file-chips { |
| 885 | + display: flex; gap: 6px; flex-wrap: wrap; padding: 4px 12px 0; |
| 886 | +} |
| 887 | +.file-chip { |
| 888 | + display: inline-flex; align-items: center; gap: 4px; |
| 889 | + background: var(--accent-subtle); border: 1px solid rgba(56,189,248,0.15); |
| 890 | + border-radius: 6px; padding: 3px 8px; font-size: 11px; font-family: var(--font-mono); |
| 891 | + color: var(--accent); animation: chipEnter 0.2s ease-out; |
| 892 | +} |
| 893 | +@keyframes chipEnter { |
| 894 | + from { opacity: 0; transform: scale(0.9); } |
| 895 | + to { opacity: 1; transform: scale(1); } |
| 896 | +} |
| 897 | +.file-chip .chip-name { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 898 | +.file-chip .chip-size { color: var(--text-tertiary); font-size: 10px; } |
| 899 | +.file-chip .chip-remove { |
| 900 | + cursor: pointer; opacity: 0.5; font-size: 14px; line-height: 1; padding: 0 2px; |
| 901 | + transition: opacity 0.1s; |
| 902 | +} |
| 903 | +.file-chip .chip-remove:hover { opacity: 1; color: #ef4444; } |
| 904 | + |
| 905 | +/* Drag-over state */ |
| 906 | +#messages.drag-over { outline: 2px dashed var(--accent); outline-offset: -8px; } |
| 907 | +#messages.drag-over::after { |
| 908 | + content: 'Drop files to attach'; position: absolute; inset: 0; |
| 909 | + display: flex; align-items: center; justify-content: center; |
| 910 | + font-size: 14px; color: var(--text-secondary); |
| 911 | + background: rgba(10,10,11,0.6); backdrop-filter: blur(4px); |
| 912 | + z-index: 100; pointer-events: none; |
| 913 | +} |
| 914 | + |
872 | 915 | /* Input area top border gradient */ |
873 | 916 | #input-area { |
874 | 917 | border-top: none; |
@@ -1046,8 +1089,13 @@ <h2>kode agent</h2> |
1046 | 1089 |
|
1047 | 1090 | <!-- Input --> |
1048 | 1091 | <div id="input-area"> |
| 1092 | + <div id="file-chips"></div> |
1049 | 1093 | <div id="completion"></div> |
1050 | 1094 | <div id="input-row"> |
| 1095 | + <button id="attach-btn" title="Attach files"> |
| 1096 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg> |
| 1097 | + </button> |
| 1098 | + <input type="file" id="file-input" multiple style="display:none"/> |
1051 | 1099 | <div id="input-wrap"> |
1052 | 1100 | <textarea id="prompt" rows="1" placeholder="Ask odek to do something... (@ to reference)"></textarea> |
1053 | 1101 | </div> |
@@ -1121,6 +1169,7 @@ <h3>Delete session?</h3> |
1121 | 1169 | let busy = false; |
1122 | 1170 | let history = JSON.parse(localStorage.getItem('kode_history') || '[]'); |
1123 | 1171 | let historyIdx = -1; |
| 1172 | +let attachedFiles = []; // {name, size, content} |
1124 | 1173 |
|
1125 | 1174 | // ── DOM ── |
1126 | 1175 | const messagesEl = document.getElementById('messages'); |
@@ -1886,7 +1935,17 @@ <h3>Delete session?</h3> |
1886 | 1935 |
|
1887 | 1936 | // ── Send ── |
1888 | 1937 | function send() { |
1889 | | - const text = promptEl.value.trim(); |
| 1938 | + let text = promptEl.value.trim(); |
| 1939 | + |
| 1940 | + // Prepend attached file content |
| 1941 | + if (attachedFiles.length > 0) { |
| 1942 | + const blocks = attachedFiles.map(f => |
| 1943 | + '--- ' + f.name + ' (' + formatFileSize(f.size) + ') ---\n' + f.content + '\n--- end ' + f.name + ' ---' |
| 1944 | + ); |
| 1945 | + text = blocks.join('\n\n') + '\n\n' + text; |
| 1946 | + clearAttachedFiles(); |
| 1947 | + } |
| 1948 | + |
1890 | 1949 | if (!text || busy || !ws || ws.readyState !== WebSocket.OPEN) return; |
1891 | 1950 |
|
1892 | 1951 | history.push(text); |
@@ -1925,6 +1984,105 @@ <h3>Delete session?</h3> |
1925 | 1984 | })); |
1926 | 1985 | } |
1927 | 1986 |
|
| 1987 | +// ── File Attachments ── |
| 1988 | +const fileInput = document.getElementById('file-input'); |
| 1989 | +const attachBtn = document.getElementById('attach-btn'); |
| 1990 | +const fileChips = document.getElementById('file-chips'); |
| 1991 | + |
| 1992 | +function formatFileSize(bytes) { |
| 1993 | + if (bytes < 1024) return bytes + ' B'; |
| 1994 | + if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB'; |
| 1995 | + return (bytes/(1024*1024)).toFixed(1) + ' MB'; |
| 1996 | +} |
| 1997 | + |
| 1998 | +function addAttachedFile(file) { |
| 1999 | + // Check total size (max 10MB total) |
| 2000 | + const totalSize = attachedFiles.reduce((s, f) => s + f.size, 0) + file.size; |
| 2001 | + if (totalSize > 10 * 1024 * 1024) { |
| 2002 | + showToast('Total attachment size exceeds 10 MB'); |
| 2003 | + return; |
| 2004 | + } |
| 2005 | + attachedFiles.push(file); |
| 2006 | + renderFileChips(); |
| 2007 | +} |
| 2008 | + |
| 2009 | +function removeAttachedFile(index) { |
| 2010 | + attachedFiles.splice(index, 1); |
| 2011 | + renderFileChips(); |
| 2012 | +} |
| 2013 | + |
| 2014 | +function clearAttachedFiles() { |
| 2015 | + attachedFiles = []; |
| 2016 | + renderFileChips(); |
| 2017 | +} |
| 2018 | + |
| 2019 | +function renderFileChips() { |
| 2020 | + if (attachedFiles.length === 0) { |
| 2021 | + fileChips.innerHTML = ''; |
| 2022 | + return; |
| 2023 | + } |
| 2024 | + fileChips.innerHTML = attachedFiles.map((f, i) => |
| 2025 | + '<span class="file-chip">' + |
| 2026 | + '<span class="chip-name">' + escapeHtml(f.name) + '</span>' + |
| 2027 | + '<span class="chip-size">' + formatFileSize(f.size) + '</span>' + |
| 2028 | + '<span class="chip-remove" onclick="removeAttachedFile(' + i + ')">✕</span>' + |
| 2029 | + '</span>' |
| 2030 | + ).join(''); |
| 2031 | +} |
| 2032 | + |
| 2033 | +function readFileAsText(file) { |
| 2034 | + return new Promise((resolve, reject) => { |
| 2035 | + // Limit individual files to 5MB |
| 2036 | + if (file.size > 5 * 1024 * 1024) { |
| 2037 | + reject(new Error('File too large (max 5 MB): ' + file.name)); |
| 2038 | + return; |
| 2039 | + } |
| 2040 | + const reader = new FileReader(); |
| 2041 | + reader.onload = () => resolve(reader.result); |
| 2042 | + reader.onerror = () => reject(reader.error); |
| 2043 | + reader.readAsText(file); |
| 2044 | + }); |
| 2045 | +} |
| 2046 | + |
| 2047 | +function handleFiles(fileList) { |
| 2048 | + const promises = []; |
| 2049 | + for (let i = 0; i < fileList.length; i++) { |
| 2050 | + const file = fileList[i]; |
| 2051 | + promises.push( |
| 2052 | + readFileAsText(file).then(content => { |
| 2053 | + addAttachedFile({name: file.name, size: file.size, content}); |
| 2054 | + }).catch(err => { |
| 2055 | + showToast(err.message); |
| 2056 | + }) |
| 2057 | + ); |
| 2058 | + } |
| 2059 | + return Promise.all(promises); |
| 2060 | +} |
| 2061 | + |
| 2062 | +// Attach button |
| 2063 | +attachBtn.addEventListener('click', () => fileInput.click()); |
| 2064 | +fileInput.addEventListener('change', () => { |
| 2065 | + handleFiles(fileInput.files); |
| 2066 | + fileInput.value = ''; |
| 2067 | +}); |
| 2068 | + |
| 2069 | +// Drag and drop on messages area |
| 2070 | +messagesEl.addEventListener('dragover', (e) => { |
| 2071 | + e.preventDefault(); |
| 2072 | + messagesEl.classList.add('drag-over'); |
| 2073 | +}); |
| 2074 | +messagesEl.addEventListener('dragleave', () => { |
| 2075 | + messagesEl.classList.remove('drag-over'); |
| 2076 | +}); |
| 2077 | +messagesEl.addEventListener('drop', (e) => { |
| 2078 | + e.preventDefault(); |
| 2079 | + messagesEl.classList.remove('drag-over'); |
| 2080 | + if (e.dataTransfer.files.length > 0) { |
| 2081 | + handleFiles(e.dataTransfer.files); |
| 2082 | + promptEl.focus(); |
| 2083 | + } |
| 2084 | +}); |
| 2085 | + |
1928 | 2086 | // ── Input handlers ── |
1929 | 2087 | promptEl.addEventListener('keydown', (e) => { |
1930 | 2088 | if (e.key === 'Enter' && !e.shiftKey) { |
|
0 commit comments