diff --git a/index.html b/index.html
index 1af303e..a939b7c 100644
--- a/index.html
+++ b/index.html
@@ -5,314 +5,306 @@
-
-
-
-
-
-
-
-
+
-
diff --git a/js/cards.js b/js/cards.js
new file mode 100644
index 0000000..1136795
--- /dev/null
+++ b/js/cards.js
@@ -0,0 +1,119 @@
+export function contentCard(content) {
+ const card = {
+ type: 'AdaptiveCard',
+ version: '1.5',
+ body: [{
+ type: 'TextBlock',
+ text: content,
+ wrap: true
+ }]
+ }
+ const adaptiveCard = new AdaptiveCards.AdaptiveCard();
+ adaptiveCard.parse(card);
+ return adaptiveCard.render();
+}
+
+export function confirmationCard(message) {
+ const card = {
+ type: 'AdaptiveCard',
+ version: '1.5',
+ body: [{
+ type: 'TextBlock',
+ text: message,
+ wrap: true
+ }],
+ actions: [{
+ type: 'Action.Submit',
+ title: 'Yes',
+ data: 'yes'
+ }, {
+ type: 'Action.Submit',
+ title: 'No',
+ data: 'no'
+ }]
+ }
+ const adaptiveCard = new AdaptiveCards.AdaptiveCard();
+ adaptiveCard.parse(card);
+ const renderedCard = adaptiveCard.render();
+ return {
+ render: () => renderedCard,
+ getAnswer: () => new Promise((resolve) => {
+ adaptiveCard.onExecuteAction = (action) => {
+ renderedCard.parentNode.removeChild(renderedCard)
+ resolve(action.data === 'yes')
+ }
+ })
+ }
+}
+
+export function questionCard(question, answers = [], defaultValue = "") {
+ const card = {
+ type: 'AdaptiveCard',
+ version: '1.5',
+ body: [{
+ type: 'TextBlock',
+ text: question,
+ wrap: true
+ }],
+ actions: answers.filter(answer => answer && answer.trim()).map(answer => ({
+ type: 'Action.Submit',
+ title: answer,
+ data: answer
+ }))
+ }
+ if (card.actions.length == 0) {
+ card.body = [{
+ type: "Input.Text",
+ id: "answer",
+ label: question,
+ value: defaultValue,
+ isMultiline: question.indexOf('prompt') > -1
+ }];
+ card.actions.push({
+ type: 'Action.Submit',
+ title: 'Save'
+ });
+ }
+ const adaptiveCard = new AdaptiveCards.AdaptiveCard();
+ adaptiveCard.parse(card);
+ const renderedCard = adaptiveCard.render();
+ return {
+ render: () => renderedCard,
+ getAnswer: () => new Promise((resolve) => {
+ adaptiveCard.onExecuteAction = (action) => {
+ renderedCard.parentNode.removeChild(renderedCard)
+ if (action.data === '_cancel_' || (typeof action.data === 'object' && !action.data.answer)) {
+ resolve()
+ } else if (typeof action.data === 'object') {
+ resolve(action.data.answer)
+ } else {
+ resolve(action.data)
+ }
+ }
+ })
+ }
+}
+
+export async function configCard(outputContainer, option, text = `Enter the value for ${option}:`, defaultValue = undefined) {
+ const value = getQueryString(option) || localStorage.getItem(option)
+ if (value) return value
+ const question = questionCard(text, undefined, defaultValue)
+ outputContainer.appendChild(question.render())
+ const answer = await question.getAnswer()
+ if (answer) {
+ localStorage.setItem(option, answer)
+ return answer
+ }
+}
+
+export function useCardMarkdownRenderer(render) {
+ AdaptiveCards.AdaptiveCard.onProcessMarkdown = function (text, result) {
+ result.outputHtml = render(text);
+ result.didProcess = true;
+ };
+}
+
+function getQueryString(key) {
+ var urlParams = new URLSearchParams(window.location.search)
+ return urlParams.get(key)
+}
\ No newline at end of file
diff --git a/js/chat.js b/js/chat.js
new file mode 100644
index 0000000..ef65fe4
--- /dev/null
+++ b/js/chat.js
@@ -0,0 +1,24 @@
+export function createConversation(systemPrompt, getCompletion, getImage, vectorSearch) {
+ const messages = [{ role: "system", content: systemPrompt }];
+ return async (content, onTextReceived = undefined, useVectorSearch = false, generateImage = false) => {
+ const useVision = Array.isArray(content)
+ if (useVectorSearch) {
+ const results = await vectorSearch(useVision ? content[0].text : content);
+ if (results.length > 0) {
+ const retrieval = results[0].text;
+ messages.push({ content: `More details: ${retrieval}`, role: 'user' });
+ }
+ }
+ messages.push({ content, role: 'user' });
+ if (generateImage) {
+ messages.push({ content: 'Create a prompt for DALL-E to generate an image. Only return the prompt, NO OTHER TEXT.', role: 'user' });
+ }
+ const response = await getCompletion(messages, onTextReceived, useVision);
+ messages.push({ content: response, role: 'assistant' });
+ if (generateImage) {
+ return await getImage(response);
+ } else {
+ return response;
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/files.js b/js/files.js
new file mode 100644
index 0000000..b9612cf
--- /dev/null
+++ b/js/files.js
@@ -0,0 +1,93 @@
+import * as pdfjsLib from 'https://mozilla.github.io/pdf.js/build/pdf.mjs';
+pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://mozilla.github.io/pdf.js/build/pdf.worker.mjs';
+
+export function onFilesDropped(element, onFilesAdded) {
+ element.addEventListener('dragover', prevent_defaults, false);
+ element.addEventListener('dragenter', prevent_defaults, false);
+ element.addEventListener('drop', async (e) => {
+ prevent_defaults(e);
+ addFiles(e.dataTransfer.files);
+ }, false);
+ return async function addFiles(files) {
+ const contents = [];
+ for (let i = 0; i < files.length; i++) {
+ const name = files[i].name;
+ const type = files[i].type;
+ try {
+ if (type === 'application/pdf') {
+ contents.push({
+ name,
+ type,
+ content: await read_pdf(files[i])
+ });
+ } else if (type.indexOf('image') === 0) {
+ contents.push({
+ name,
+ type,
+ content: await read_image(files[i])
+ })
+ } else {
+ contents.push({
+ name,
+ type,
+ content: await read_file(files[i])
+ });
+ }
+ } catch (e) {
+ console.error('FileReader error: ', e);
+ alert(`Error reading file ${name}`)
+ }
+ }
+ onFilesAdded(contents);
+ }
+}
+
+function read_file(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ resolve(event.target.result);
+ };
+ reader.onerror = reject;
+ reader.readAsText(file);
+ });
+}
+
+function read_image(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = function () {
+ resolve(reader.result);
+ }
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+}
+
+function read_pdf(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = async (event) => {
+ try {
+ const pdfData = new Uint8Array(reader.result);
+ const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
+ let content = "";
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
+ const page = await pdfDoc.getPage(i);
+ const textContent = await page.getTextContent();
+ content += textContent.items.map(item => item.str).join(" ");
+ }
+ resolve(content);
+ } catch (error) {
+ reject(error);
+ }
+ };
+ reader.onerror = reject;
+ reader.readAsArrayBuffer(file);
+ });
+}
+
+function prevent_defaults(e) {
+ e.preventDefault();
+ e.stopPropagation();
+}
\ No newline at end of file
diff --git a/js/openai.js b/js/openai.js
new file mode 100644
index 0000000..5f6f8f8
--- /dev/null
+++ b/js/openai.js
@@ -0,0 +1,173 @@
+export default function (apiKey, apiOrg) {
+ const headers = {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'OpenAI-Organization': apiOrg
+ }
+ return {
+ getCompletion: get_completion.bind(null, headers),
+ getImage: get_image.bind(null, headers),
+ playSpeech: play_speech.bind(null, headers),
+ getTranscription: get_transcription.bind(null, headers),
+ getEmbeddings: get_vectors.bind(null, headers)
+ }
+}
+
+async function get_completion(headers, messages, onTextReceived = undefined, useVision = false, max_tokens = 150) {
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
+ headers: {
+ ...headers,
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST',
+ body: JSON.stringify({
+ //TODO: not working with successive requests (since image content gets added to the messages), but acording to sama its the 'same'
+ //model: useVision ? 'gpt-4-vision-preview' : 'gpt-4-1106-preview',
+ model: 'gpt-4-vision-preview',
+ messages,
+ max_tokens,
+ temperature: 0.1,
+ top_p: 1,
+ stream: (onTextReceived !== undefined)
+ })
+ })
+ if (onTextReceived) {
+ if (!response.ok) throw new Error(await response.text());
+ if (!response.body[Symbol.asyncIterator]) {
+ response.body[Symbol.asyncIterator] = () => {
+ const reader = response.body.getReader();
+ return {
+ next: () => reader.read(),
+ };
+ };
+ }
+ let all_text = ""
+ const decoder = new TextDecoder()
+ for await (const chunk_arr of response.body) {
+ const chunk = decoder.decode(chunk_arr, { stream: true })
+ const lines = chunk.toString().trim().split("\n")
+ for (let line of lines) {
+ const data = line.indexOf('{')
+ if (data > -1) {
+ const json = line.slice(data);
+ if (json) {
+ try {
+ const data = JSON.parse(json);
+ if (data.choices && data.choices.length > 0) {
+ if (data.choices[0].finish_reason === "length" && max_tokens < 1000) {
+ onTextReceived("", true)
+ return await get_completion(headers, onTextReceived, useVision, max_tokens * 2)
+ }
+ const text = data.choices[0].delta.content;
+ if (!text) continue;
+ all_text += text;
+ onTextReceived(text);
+ }
+ } catch (e) {
+ console.error(e)
+ //TODO: handle this better
+ }
+ }
+ }
+ }
+ }
+ return all_text
+ } else {
+ const data = await response.json()
+ if (data && data.choices && data.choices[0] && data.choices[0].finish_reason === "length" && max_tokens < 1000) {
+ return await get_completion(headers, onTextReceived, useVision, max_tokens * 2)
+ }
+ else if (data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
+ return data.choices[0].message.content.trim()
+ } else {
+ return ""
+ }
+ }
+}
+
+async function get_image(headers, prompt) {
+ const response = await fetch("https://api.openai.com/v1/images/generations", {
+ headers: {
+ ...headers,
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST',
+ body: JSON.stringify({
+ prompt,
+ model: 'dall-e-3',
+ size: '1024x1024',
+ n: 1
+ })
+ })
+ const image = await response.json()
+ if (image && image.data && image.data[0] && image.data[0].url) {
+ return image.data[0].url
+ }
+}
+
+async function play_speech(headers, input, voice = 'alloy') {
+ const response = await fetch("https://api.openai.com/v1/audio/speech", {
+ headers: {
+ ...headers,
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST',
+ body: JSON.stringify({
+ input,
+ voice,
+ model: 'tts-1'
+ })
+ })
+ const audioData = [];
+ const reader = response.body.getReader();
+ const audioObj = new Audio();
+
+ reader.read().then(function processAudio({ done, value }) {
+ if (done) {
+ audioObj.src = URL.createObjectURL(new Blob(audioData));
+ audioObj.play();
+ document.body.onclick = () => {
+ audioObj.pause();
+ document.body.onclick = undefined;
+ }
+ return;
+ }
+ audioData.push(value);
+ return reader.read().then(processAudio);
+ });
+}
+
+async function get_transcription(headers, audioBlob) {
+ const formData = new FormData();
+ formData.append('file', audioBlob, 'recording.webm');
+ formData.append('model', 'whisper-1');
+ const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
+ headers,
+ method: 'POST',
+ body: formData
+ })
+ const data = await response.json()
+ return data.text
+}
+
+async function get_vectors(headers, input) {
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
+ headers: {
+ ...headers,
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST',
+ body: JSON.stringify({
+ input,
+ model: 'text-embedding-ada-002'
+ })
+ })
+ const embeddings = await response.json()
+ if (embeddings && embeddings.data) {
+ return input.map((input, i) => ({
+ embedding: embeddings.data[i].embedding,
+ text: input
+ }))
+ } else {
+ return []
+ }
+}
\ No newline at end of file
diff --git a/js/vectors.js b/js/vectors.js
new file mode 100644
index 0000000..0120dac
--- /dev/null
+++ b/js/vectors.js
@@ -0,0 +1,84 @@
+export default function createVectorStore(getEmbeddings) {
+ const db = [];
+ return {
+ vectorSearch: vector_search.bind(null, db, getEmbeddings),
+ storeDocument: store_document.bind(null, db, getEmbeddings),
+ storeVector: store_vector.bind(null, db),
+ retrieveVectors: retrieve_vectors.bind(null, db)
+ }
+}
+
+async function vector_search(embeddingsDB, getEmbeddings, text) {
+ const embeddings = await getEmbeddings([text]);
+ const single = embeddings[0];
+ if (!single) return [];
+ const results = retrieve_vectors(embeddingsDB, single.embedding);
+ return results;
+}
+
+async function store_document(embeddingsDB, getEmbeddings, name, text) {
+ for (const chunk of chunk_text(text, 500)) {
+ if (chunk.length == 0) break;
+ if (!chunk.join('').trim()) break;
+ const embeddings = await getEmbeddings(chunk);
+ for (const { embedding, text } of embeddings) {
+ store_vector(embeddingsDB, name, text, embedding);
+ }
+ }
+}
+
+function store_vector(embeddingsDB, name, text, embedding) {
+ embeddingsDB.push({ name, text, embedding });
+}
+
+function retrieve_vectors(embeddingsDB, target_embedding, count = 5) {
+ const results = embeddingsDB.map(({ name, text, embedding }) => ({
+ name,
+ text,
+ relevance: cosine_similarity(embedding, target_embedding),
+ })).slice(0, count);
+ results.sort((a, b) => b.relevance - a.relevance);
+ return results;
+}
+
+function cosine_similarity(vecA, vecB) {
+ let dotProduct = 0.0, normA = 0.0, normB = 0.0;
+ for (let i = 0; i < vecA.length; i++) {
+ dotProduct += vecA[i] * vecB[i];
+ normA += vecA[i] * vecA[i];
+ normB += vecB[i] * vecB[i];
+ }
+ if (normA === 0 || normB === 0) return 0;
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
+}
+
+function* chunk_text(text, maxChunkSize) {
+ const sentences = text.match(/[^.!?]+[.!?]*/g) || [text];
+ let chunk = '';
+ let batch = [];
+ const maxBatchSize = 20;
+
+ for (let sentence of sentences) {
+ sentence = sentence.trim();
+ if ((chunk + ' ' + sentence).length <= maxChunkSize) {
+ chunk += ' ' + sentence;
+ } else {
+ if (chunk.trim()) {
+ batch.push(chunk.trim());
+ if (batch.length === maxBatchSize) {
+ yield batch;
+ batch = [];
+ }
+ }
+ chunk = sentence;
+ }
+ }
+
+ if (chunk.trim()) {
+ batch.push(chunk.trim());
+ }
+
+ if (batch.length) {
+ yield batch;
+ }
+}
\ No newline at end of file
diff --git a/single_page.html b/single_page.html
new file mode 100644
index 0000000..1af303e
--- /dev/null
+++ b/single_page.html
@@ -0,0 +1,877 @@
+
+
+
+
+
+
+
gpt-spa - Scalable Dynamics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..46d4bd0
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,432 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 16px;
+ line-height: 1.6;
+ background-color: #1A202C;
+ color: #fff;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow-y: auto;
+}
+
+.container {
+ display: flex;
+ flex-grow: 1;
+}
+
+aside {
+ width: 250px;
+ padding: 20px;
+ background-color: #000;
+ color: #fff;
+ height: 100vh;
+ overflow-y: auto;
+ position: fixed;
+}
+
+aside h2 {
+ margin-bottom: 20px;
+}
+
+aside ul {
+ list-style: none;
+}
+
+aside ul li {
+ margin-bottom: 2px;
+}
+
+aside ul li a {
+ color: #fff;
+ text-decoration: none;
+ display: block;
+ padding: 4px 6px;
+ line-height: 30px;
+}
+
+aside ul li a img {
+ width: 24px;
+ height: 24px;
+ margin-right: 10px;
+ vertical-align: middle;
+ border-radius: 50%;
+}
+
+aside ul li a:hover {
+ color: #f4f4f4;
+ background-color: #1A202C;
+ border-radius: 6px;
+}
+
+main {
+ flex-grow: 1;
+ padding: 20px;
+ position: relative;
+ margin-left: 250px;
+}
+
+main h1 {
+ position: absolute;
+}
+
+.tabs {
+ background: #4A5568;
+ width: 350px;
+ margin: 0 auto;
+ border-radius: 6px;
+}
+
+.tab {
+ display: inline-block;
+}
+
+.tab>label {
+ background: transparent;
+ padding: 10px;
+ font-size: 18px;
+ width: 150px;
+ height: 40px;
+ line-height: 32px;
+ text-align: center;
+ display: inline-block;
+ cursor: pointer;
+ padding: 4px 20px;
+ margin: 6px 10px;
+}
+
+.tab>[type=radio] {
+ display: none;
+}
+
+.content {
+ position: absolute;
+ inset: 0;
+ top: 100px;
+ padding: 20px;
+ display: none;
+}
+
+[type=radio]:checked~label {
+ background: #2D3748;
+ border-radius: 6px;
+}
+
+[type=radio]:checked~label~.content {
+ display: block;
+}
+
+.output-container {
+ padding: 20px;
+ padding-bottom: 100px;
+}
+
+.output-message {
+ background: #2D3748;
+ border-radius: 10px;
+ padding: 15px;
+ margin-bottom: 15px;
+}
+
+.output-message h3 {
+ font-size: 18px;
+ margin-bottom: 10px;
+}
+
+.output-message p {
+ font-size: 16px;
+ line-height: 1.4;
+}
+
+
+.output-message img {
+ max-width: 256px;
+ height: auto;
+}
+
+.input-container {
+ position: fixed;
+ bottom: 0;
+ left: 250px;
+ right: 0;
+ height: 50px;
+}
+
+.input-container .input-content {
+ position: relative;
+ min-width: 500px;
+ width: 50%;
+ margin: 0 auto;
+}
+
+.input-container label input {
+ display: none;
+}
+
+.input-content>textarea,
+.input-content>input[type="text"] {
+ padding: 4px 40px;
+ height: 40px;
+ line-height: 30px;
+}
+
+.input-container button,
+.input-container label {
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ position: absolute;
+ top: 4px;
+ line-height: 30px;
+ height: 40px;
+ width: 40px;
+ padding: 4px 6px;
+ z-index: 1;
+}
+
+.input-container label {
+ left: 2px;
+}
+
+.input-container button {
+ right: 2px;
+}
+
+.form {
+ width: 50%;
+ margin: 20px auto;
+}
+
+textarea,
+select,
+input[type="text"],
+input[type="file"] {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 10px;
+ border-radius: 5px;
+ border: 1px solid #ccc;
+ background-color: #2D3748;
+ color: #fff;
+ outline: none;
+ resize: none;
+}
+
+aside button,
+.form button,
+.ac-pushButton {
+ background-color: #10A37F;
+ border: none;
+ color: #fff;
+ border-radius: 6px;
+ cursor: pointer;
+ margin: 0 auto;
+ margin-top: 12px;
+ display: block;
+ width: 100%;
+ height: 30px;
+ padding: 4px 6px;
+}
+
+.form label {
+ display: block;
+}
+
+.form button {
+ display: inline-block;
+ margin: 5px;
+ width: 150px;
+}
+
+.buttons {
+ margin: 20px 0;
+}
+
+#delete-button {
+ background-color: #E53E3E;
+}
+
+#cancel-button {
+ background-color: #718096;
+}
+
+#cancel-button,
+#save-button {
+ float: right;
+}
+
+.input-container {
+ position: fixed;
+ left: 250px;
+ bottom: 0;
+ right: 25px;
+ background-color: #1A202C;
+ padding: 10px;
+ height: 80px;
+}
+
+.input-content {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.ac-adaptiveCard {
+ background-color: #4A5568 !important;
+ color: white !important;
+ border-radius: 10px;
+ padding: 15px;
+ margin-bottom: 15px;
+}
+
+.ac-textBlock,
+.ac-textRun {
+ color: white !important;
+}
+
+.ac-pushButton {
+ margin-top: 0;
+}
+
+#sidebar-toggle,
+#sidebar-toggle~label {
+ display: none;
+}
+
+.loader {
+ display: inline-block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+}
+
+.loader div {
+ position: absolute;
+ top: 33px;
+ width: 13px;
+ height: 13px;
+ border-radius: 50%;
+ background: white;
+ animation-timing-function: cubic-bezier(0, 1, 1, 0);
+}
+
+.loader div:nth-child(1) {
+ left: 8px;
+ animation: loader1 0.6s infinite;
+}
+
+.loader div:nth-child(2) {
+ left: 8px;
+ animation: loader2 0.6s infinite;
+}
+
+.loader div:nth-child(3) {
+ left: 32px;
+ animation: loader2 0.6s infinite;
+}
+
+.loader div:nth-child(4) {
+ left: 56px;
+ animation: loader3 0.6s infinite;
+}
+
+@keyframes loader1 {
+ 0% {
+ transform: scale(0);
+ }
+}
+
+@keyframes loader3 {
+ 0% {
+ transform: scale(0);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes loader2 {
+ 0% {
+ transform: translate(0, 0);
+ }
+
+ 100% {
+ transform: translate(24px, 0);
+ }
+}
+
+@media (max-width: 768px) {
+ aside {
+ padding: 10px;
+ left: -250px;
+ z-index: 100;
+ transition: left 0.3s ease-in-out;
+ }
+
+ #sidebar-toggle:checked~aside {
+ left: 0;
+ }
+
+ #sidebar-toggle~label {
+ position: fixed;
+ top: 27px;
+ left: 8px;
+ z-index: 99;
+ font-size: 24px;
+ display: block;
+ }
+
+ #sidebar-toggle:checked~label {
+ inset: 0;
+ }
+
+ main {
+ margin-left: 0;
+ }
+
+ main h1 {
+ display: none;
+ }
+
+ .content {
+ top: 50px;
+ }
+
+ .input-container {
+ flex-direction: column;
+ align-items: center;
+ left: 0;
+ right: 0;
+ }
+
+ .input-container .input-content {
+ flex-direction: column;
+ width: 100%;
+ min-width: auto;
+ margin: 0;
+ }
+
+ .form {
+ width: 100%;
+ }
+
+ .form button {
+ width: calc((100% - 80px) / 3);
+ }
+ .form label,
+ .form textarea,
+ .form select,
+ .form .buttons,
+ .form input[type="text"],
+ .form input[type="file"] {
+ width: calc(100% - 60px);
+ margin-left: 30px;
+ }
+}
\ No newline at end of file