diff --git a/about.txt b/about.txt index 65cd6f9..dc621d6 100644 --- a/about.txt +++ b/about.txt @@ -1,5 +1,3 @@ -Made By Daniel Enesi, do not copy, do not pirate. +Made By Daniel Enesi. Do not misuse. -enesidaniel.120064@gmail.com, +2347010081471 - -Asks interview questions and records answers, either in text or in voice +Asks interview questions and records answers, either in text or in video. diff --git a/classes.js b/classes.js new file mode 100644 index 0000000..5114237 --- /dev/null +++ b/classes.js @@ -0,0 +1,266 @@ +class Question{ + constructor(question="What do you want to be asked?"){ + this._question = question; + this.build(); + this.event(); + Question.questions.push(this); + } + get question(){ + return this.text.value; + } + set question(ask){ + this.text.value = ask; + } + build(){ + add( + (this.text = make("input")) + , add(this.box = make("li"), QUESTIONLIST) + ).value = this._question; + this.text.type = "text"; + + ["mover", "deleter"].forEach((type, i)=>{ + add( + (this[type] = make("button")), this.box + ).className = "icon"; + this[type].textContent = Question.iconValues[i]; + }); + ["box", "text", "mover", "deleter"].forEach(dom=>this[dom].obj=this); + this.mover.draggable = true; + } + event(){ + this.deleter.onclick=()=>this.delete(); + this.text.onkeyup=(event)=>{ + event.key=="Enter" && event.ctrlKey && (new Question).text.focus(); + } + this.mover.onkeydown=(event)=>{ + Question.keys.includes(event.key) && event.preventDefault(); + } + this.mover.onkeyup=(event)=>{ + if (Question.keys.includes(event.key) && + this.box[`${Question.keyMap[event.key]}ElementSibling`]){ + event.preventDefault(); + if (event.key.includes("Up")){ + QUESTIONLIST.insertBefore(this.box, this.box.previousElementSibling); + } else{ + QUESTIONLIST.insertBefore(this.box.nextElementSibling, this.box); + } + this.mover.focus(); + } + } + this.mover.ondragstart=(event)=>{ + this.box.classList.add("highlight"); + } + this.mover.ondragend=(event)=>{ + let at = document.elementFromPoint(event.x, event.y); + if (at && at.obj && at.obj != this){ + QUESTIONLIST.insertBefore(this.box, at.obj.box) + } + this.box.classList.remove("highlight"); + } + // this.mover.onmouse + this.text.onblur=()=>{ + if (!this.question.trim()){ + this.question = this._question; + } + } + } + delete(){ + this.box.remove(); + remove(this, Question.questions); + Question.deletedQuestions.push(this); + } + restore(){ + QUESTIONLIST.append(this.box); + } + static restoreLast(){ + SETUP.checkVisibility() && Question.deletedQuestions.length && Question.deletedQuestions.pop().restore(); + } + static questions = []; + static iconValues = ['<>', 'X']; + static deletedQuestions = []; + static keyMap = {"ArrowUp": "previous", "ArrowDown": "next"}; + static keys = Object.keys(Question.keyMap); +} + +class Switch{ + constructor(where, obj, property, values){ + [this.container, this.obj, this.property, this.values] = arguments; + this.build(); + this.event(); + Switch.swtiches.push(this); + } + build(){ + add((this.switch = make()) + , add((this.holder = make()), this.container) + ).className = "switch"; + this.holder.className = "switchold"; + // this.switch.tabIndex = -1; + } + event(){ + this.switch.onclick = (event)=>{ + event.preventDefault(); + this.holder.classList.contains("off") + ?this.holder.classList.remove("off") + :this.holder.classList.add("off"); + this.action(); + } + } + get on(){ + return !this.holder.classList.contains("off") + } + action(){ + // all switches will set a property of something + // between to two values (flex/none) (true/false) + // 1/0 + let [obj, property, values] = [this.obj, this.property, this.values]; + obj[property] = obj[property] == values[0]?values[1]:values[0] + } + static swtiches = []; +} + +class Help{ + constructor(){ + this.messages = [ + "Use ctrl+z to restore last delted question" + , "Hit enter on a question box to add another question" + , "etc" + ] + } +} + +class SearchUI{ + // to specific + constructor(where, values){ + [this.container, this._values] = arguments; + this.build(); + this.event(); + } + get values(){ + if (typeof this._values == "function"){ + return this._values(); + } else{ + return this._values; + } + } + build(){ + add((this.searchBox = make("input")) + , add((this.box = make()) + , this.container)).type = "text"; + this.box.className = "searchui"; + add((this.list = make("ul")), this.box).hidden = true; + this.list.size = 20 + } + event(){ + // change onvoice... to event variable/not + speechSynthesis.onvoiceschanged=(event)=>{ + let voices = this.values; + for (let i in voices){ + add(make("li"), this.list).textContent = voices[i]; + } + } + this.searchBox.onfocus=()=>this.list.hidden=false; + this.searchBox.addEventListener("blur" + , ()=>setTimeout(()=>this.list.hidden=true, 500)); + this.searchBox.oninput=(event)=>{ + [...this.list.children].forEach(child=>{ + child.hidden = !(child.textContent.toLowerCase().includes(this.searchBox.value.toLowerCase())); + }); + if (this.value){ + TALK.voice = getVoice(this.value); + } + } + this.list.onclick=(event)=>{ + event.stopImmediatePropagation(); + event.stopPropagation(); + if (event.target.parentElement == this.list){ + this.value = event.target.textContent; + } + } + } + get value(){ + if (this.values.includes(this.searchBox.value)){ + return this.searchBox.value; + }; + return false; + } + set value(name){ + this.searchBox.value = name; + TALK.voice = getVoice(name); + } +} + +// modals +class Modal{ + constructor(buttons){ + this.buttons = buttons; + this.message = ""; + this.build(); + this.event(); + Modal.modals.push(this); + } + build(){ + add(this.modal = make()).className = "modal"; + add(this.messageBox = make('p'), this.modal); + this.buttons.forEach((button, pos)=>{ + add(this[button] = make("button"), this.modal).textContent = button; + this[button].pos = pos; + }) + } + event(){ + this.modal.onclick=(event)=>{ + if (event.target.nodeName == "BUTTON"){ + this.method(event.target.pos); + this.close(); + } + } + } + changeButtons(buttons){ + this.buttons.forEach((button, pos)=>{ + this[button].textContent = buttons[pos]; + this[button].pos = pos; + this[buttons[pos]] = this[button]; + }) + this.buttons = buttons; + } + open(message, buttons){ + // do not do anything if a modal is already up + if (Modal.showing) { + this.blink(); + return; + }; + showLoading() + if (buttons) this.changeButtons(buttons); + this.messageBox.innerHTML = message; + this.modal.classList.add("shown"); + this[this.buttons[this.buttons.length-1]].focus(); + // promise + // will be given to the event onclick + return new Promise((resolve)=>this.method = resolve); + } + close(){ + this.modal.classList.remove("shown", "blink"); + hideLoading(); + } + blink(){ + this.modal.classList.add("blink"); + setTimeout(()=>this.modal.classList.remove("blink"), 500); + } + get shown(){ + return this.modal.classList.contains("shown"); + } + static get showing(){ + return Modal.modals.some(modal=>modal.shown); + } + static modals = []; + static alertBox = new Modal(["CLOSE"]); + static confirmBox = new Modal(["CANCEL", "OK"]); +} +function alert(message, buttons){ + return Modal.alertBox.open(message, buttons); +} +function confirm(message, buttons){ + return Modal.confirmBox.open(message, buttons); +} + +// maise: young girl +// make search better by giving properties \ No newline at end of file diff --git a/events.js b/events.js index 0914301..29d0af2 100644 --- a/events.js +++ b/events.js @@ -1,118 +1,61 @@ -//all functions and variables if (except all event based) here -onbeforeunload=()=>{ - speechSynthesis.cancel(); - localStorage.questions = JSON.stringify([...questionslist.children].map(i=>i.querySelector('.question').innerText)); -} -addquestion.onclick=()=>{ - makeQuestion('Write a question here, write a question here', questionslist.childElementCount+1) -} -startbutton.onclick=()=>{ - if (!(questions=[...questionslist.children].map(i=>i.querySelector('.question').innerText)).length) return errors.innerHTML = 'No question added, Please add at least one questions to proceed'; - if (!confirm('Proceed?')) return - embassy.style.display='flex'; setup.style.display='none'; - shouldusevideo.checked?you.style.display='none':interviewbox.style.display='none'; - questionbox.innerText=(currentQuestion=questions[index])//setup non-video - if (!shouldusevideo.checked) return; - // while (!voice){} - loading.loading = 1; - userMedia = navigator.mediaDevices.getUserMedia({ - audio: true, video: true, - facingMode: {exact: "user"} - }); - // use transform scale(-1) on a container containing the video. - userMedia.then(mediaStream=>{ - //setup video recording - stream = mediaStream; - recorder = new MediaRecorder(mediaStream) - recorder.ondataavailable=()=>{ - loading.loading = 1; - finalFile = new Blob([event.data], {type:"video/mp4"}) - response.src=URL.createObjectURL(finalFile); - refresher.style.display='block'; - responseHolder.style.display='flex'; - loading.loading = 0; - } - recorder.start(); - video.srcObject = mediaStream; video.muted=true; - }).catch(error=>{alert('an error occurred, exitting!!'); location.reload()}) - video.onloadstart=()=>{ - loading.loading=0; - video.play(); - let hours = new Date(Date.now()).getHours(), time; - if(hours>16){ - time ='evening' - }else if(hours>11){ - time='afternoon' - }else if(hours>=0){ - time='morning' - } - question = new SpeechSynthesisUtterance(` - Good ${time}}. Welcome to your interview. - Please click the repeat button to repeat any asked question, - and click the forward button to go to the next question.`); - question.voice =voice//= voices[+getAll('[name=voice]').filter(v=>v.checked)[0].value] - voice; question.rate=.8; - speechSynthesis.speak(question); - question.onstart=()=>say.disabled=forward.disabled=true; - question.onend = ()=>{setTimeout(()=>!video.paused&&(say.disabled=forward.disabled=false), randBtw(1000, 2500))}; - // setup q and a - } - -} -answerquestion.onclick=()=>{ - event.preventDefault(); - if (!answerbox.reportValidity()) return; - answers.push(answerbox.value); answerbox.value=''; - if(index+1>=questions.length){ - //submit - embassy.style.display='none' - responses.style.display='flex'; - questions.forEach((q, i)=>{ - let r = responses.insertRow(); - (r.insertCell()).innerText = q; - (r.insertCell()).innerText = answers[i] - }) - return refresher.style.display='block' +// //all functions and variables if (except all event based) here +onload=()=>{ + // Turn all elements with ID into variables + identify(); + // make LOADING DIV + loader(); + // show only SETUP + switchScreen("SETUP"); + // restore saved questions + restoreSavedData(); + // consent to recording + consentToRecording(); + + // TESTS + // a = alert("Hi!").then(console.log); + // videoSwitch.switch.click() +} +// add event listeners +addEventListener("keyup", (event)=>{ + if (event.key == 'b' && event.ctrlKey){ + Question.restoreLast(); } - questionbox.innerText=(currentQuestion = questions[++index]) - if(index+2>questions.length) answerquestion.innerText='End interview'; +}) +// add question button +ADDQUESTION.onclick=()=>new Question + +// restore question button +RESTOREQUESTION.onclick=()=>Question.restoreLast(); + +// TESTing voices +TESTVOICE.onclick =()=>{ + speechSynthesis.cancel(); + if (!speechSearch.value) return; + say(`Hi, this is speech synthesis, using ${TALK.voice.name}`) } -playorpause.onclick=()=>{ - if (video.paused) { - speechSynthesis.resume(); recorder.resume(); video.play(); playorpause.innerText = 'Pause Recording'; - say.disabled=forward.disabled=false; - } else { - speechSynthesis.pause(); recorder.pause(); video.pause();playorpause.innerText = 'Continue Recording'; - question.onstart(); + +onbeforeunload=()=>{ + speechSynthesis.cancel(); + saveData();} + +STARTBUTTON.onclick=()=>{ + saveData(); + questions = JSON.parse(localStorage.questions); + if (!questions.length){ + return alert("You have to add at least one question"); + } else if (videoSwitch.on && !speechSearch.value) { + return alert("Please, choose a voice!"); } -} -forward.onclick=()=>{ - question.onstart(); - if(index>=questions.length){ - //submit - question.text = after.value?after.value:after.placeholder; - speechSynthesis.speak(question) - question.onend=()=>{ - embassy.style.display='none' - stream.getTracks().forEach(i=>i.stop()); - recorder.stop(); + confirm("Are you sure you want to begin?", ["No", "Yes"]) + .then(resp=>{ + if (resp){ + ( + interview = new Interview( + JSON.parse(localStorage.questions) + , videoSwitch.on + )).start(); + } else{ + alert("Make changes then click START"); } - return - } - question.text=(currentQuestion=questions[index++]); - speechSynthesis.speak(question) - if(index+1>questions.length) forward.innerText='End interview'; -} -say.onclick=()=>{ - speechSynthesis.cancel(); - speechSynthesis.speak(question); -} -// addquestion.click() -//for downloading -save.onclick=()=>{ - downloader.href=response.src; - downloader.click(); -} -getAll('[name=voice]').forEach(v=>{ - v.onchange=()=>{voice=speechSynthesis.getVoices()[+v.value]} -}) + }) +} \ No newline at end of file diff --git a/funcs.js b/funcs.js index 1469a46..7e3e940 100644 --- a/funcs.js +++ b/funcs.js @@ -1,16 +1,28 @@ -//make, get, identify, getall, randbtw +// dom functions let make = (name='div')=>document.createElement(name) , get = (id)=>document.getElementById(id) , getE = (selector,value)=>document.querySelector(`[${selector}=${value}]`) , getS = (query)=>document.querySelector(query) - , getAll = (query)=>[...document.querySelectorAll(query)]; -let identify = ()=>getAll('[id]').forEach(i=>window[i.id] = i) + , getAll = (query)=>[...document.querySelectorAll(query)] + , identify = ()=>getAll('[id]').forEach(i=>window[i.id] = i) , add = (what,to=document.body)=>to.appendChild(what) - , bx = (who)=>who.getBoundingClientRect(); + , bx = (who)=>who.getBoundingClientRect() + , show = (what) => what.style.display="" + , hide = (what) => what.style.display="none" + , remove = (what, from) => from.splice(from.indexOf(what), 1); -function randBtw(x=0, y=0, prec=0) { - let n = `${(y - x + 1) * Math.random() + x}`; - let s = n.split('.') - , N = s[0] + s[1].slice(0, prec) - return Number(N) + +// switch screen (still DOM) +function switchScreen(screenID){ + getAll("body>div").forEach(div=>{if (div.id) div.style.display = "none"}); + get(screenID).style.display = ""; +} + +// math +// not inclusive of the last one (a is the little one) +function randBtw(a, b){ + return a+parseInt((b-a)*Math.random()); } + +// choice +choice = (array)=>array[randBtw(0, array.length)] \ No newline at end of file diff --git a/index.html b/index.html index 989b9b5..8bba9fa 100644 --- a/index.html +++ b/index.html @@ -1,69 +1,104 @@ - - + + + + + + AI interview + + - - AI interview - - + + +

+ +

AI interview

+
+
+ Questions the AI should ask you +
+ + +
+
+ +
+ Toggle video usage: +
+
+ +
+ + Choose Voice: + + +
+
+ +
- -

AI interview

-
-
- Questions the AI should ask you - +
+ + + +

Stay focused. You can do this!

+
- -
  • - Input text to say after interview: - -
  • -
    -

    Choose Voice:

    - - - + +
    +

    + If you had two people, one of them who knows your name. + How will you tell your name to the other person? +

    + +
    - -

    - -
    -
    -
    - -
    - - - -
    - + +
    + + + + + +
    QuestionsAnswers
    +
    -
    -

    - - -
    -
    - - - - - - - - -
    Your Responses
    QuestionsAnswers
    -
    - - -
    - - - - - - - \ No newline at end of file +
    + + +
    + + + + + + + + + + \ No newline at end of file diff --git a/interview.js b/interview.js new file mode 100644 index 0000000..2b759b2 --- /dev/null +++ b/interview.js @@ -0,0 +1,292 @@ + +class Interview{ + constructor(questions, useVideo){ + window.x = this; + [this.questions, this.useVideo] = [questions, useVideo]; + this.answers = []; + this.index = 0; + this.configure(); + } + get question(){ + return this.questions[this.index]; + } + get answer(){ + return this.answers[this.index]; + } + configure(){ + // general + // choose the container video/text + this.container = Interview.containers[this.useVideo+0]; + this.container.append(CONTROLS); + this.layoutButton = new Switch(CONTROLS + , CONTROLS, "layout", [1, 0]); + switchScreen(this.container.id); + // events + this.event() + // end general + // video specific + this.questionBox = this.useVideo?CC:QUESTIONBOX; + SAY.remove(); + if (this.useVideo){ + this.layoutButton.switch.click(); // switch layout + return; + } + // end video specific + + PLAYORPAUSE.remove(); + /* this will be refactored to sth else: SAY.textcontent=REWRITE... + there will be up to three pregenerated text for answer + probably in this.suggestions = ...[[], []] + this.get:suggestion(): + choice(this.suggestions[this.index]) + for both text and video interview + */ + + } + event(){ + CONTROLS.onclick=(event)=>{ + event.stopImmediatePropagation(); + event.stopPropagation(); + let target = event.target; + switch (event.target.id) { + case "PREV": + this.previous(); + break; + + case "NEXT": + this.next(); + break; + + case "PLAYORPAUSE": + this[target.textContent.toLowerCase()](); + target.textContent = target.textContent == "PAUSE"?"RESUME":"PAUSE"; + break; + + default: + break; + } + } + if (this.useVideo){ + // TALK.voice = getVoice(speechSearch.value); + TALK.onstart = TALK.onresume = VIDEO.onpause = ()=>{ + this.disenable(); + // disable repeat, previous, next + } + TALK.onerror=()=>console.log; + + // VIDEO event (try onload too, "DOMCONTENTLOADED...") + // the real 'start' for video usage + VIDEO.onloadstart=()=>{ + hideLoading(); + this.disenable(); + VIDEO.play(); + let hours = new Date().getHours(), time; + if (hours > 17){ + time = "evening"; + } else{ + time = hours > 11?"afternoon":"morning"; + } + say(`Good ${time}. ${FIRSTSAY.value}`).then(begin=>{ + this.ask(0); + }) + } + + return; + } + ANSWERBOX.oninput=()=>{ + this.answers[this.index] = ANSWERBOX.value; + } + } + disenable(bool=true){ + [...SAY.children].forEach(i=>i.disabled=bool); + [PREV, NEXT].forEach(button=>button.disabled=bool); + } + ask(){ + if (this.useVideo){ + say(this.question).then(resp=>{ + this.disenable(false); + }); + } else{ + this.questionBox.textContent = this.question; + ANSWERBOX.value = this.answer?this.answer:""; + ANSWERBOX.focus(); + } + } + start(){ + if (this.useVideo){ + // say setting things up + showLoading("setting up..."); + say("setting things up, the interview will start soon").then(resp=>{ + if (!navigator.mediaDevices){ + return this.exit("Sorry, your browser cannot record video."); + } + // remove audio: true from getUserMedia later, screen record will + // handle that + // options for recording... + navigator.mediaDevices.getUserMedia({ + audio: true, video: true, facingMode: {exact: "user"} + }).then(mediaStream=>{ + navigator.mediaDevices.getDisplayMedia({ + audio: true, video: true + }).then(screenStream=>{ + // check if user followed instructions. + let track = screenStream.getAudioTracks()[0]; + if (!track || track.label != "System Audio"){ + throw "Did not follow instructions"; + } + this.video(mediaStream, screenStream); + }).catch(error=>{ + console.log(error); + this.exit("You must share entire screen and system audio."); + }) + }).catch((error)=>{ + console.log(error); + this.exit("You should allow all permissions!"); + }) + }) + + // ask for video, audio permission, + // add a mediarecorder attribute + // give it to VIDEO as a src + // play video + // more events for pause/end/ondataavailable in this.event + + // ask for record screen (only this page), will be needed for audio + // (check if you can crop video off the recorded portion) + return; + } + this.ask(0); + } + video(mediaStream, screenStream){ + // for playing around, add the screen's stream + // get the video track + this.mdStm = mediaStream; + this.videoTrack = mediaStream.getVideoTracks()[0]; + // stop screen's videoTrack + let screenVideo = screenStream.getVideoTracks()[0]; + screenVideo.stop(); + screenStream.removeTrack(screenVideo); + // merge the two streams into one audio stream + let audioCtx = new AudioContext() + , destination = audioCtx.createMediaStreamDestination(); + [...arguments].forEach(stream=>{ + audioCtx.createMediaStreamSource(stream).connect(destination); + }) + this.dest = destination.stream; + // get the one audio track from the gotten stream + this.audioTrack= destination.stream.getAudioTracks()[0]; + + // create a new media stream to be used + this.mediaStream = new MediaStream(); + // add video and audio tracks to this one stream + ["audio", "video"].forEach(track=>{ + this.mediaStream.addTrack(this[track+"Track"]); + }); + this.mediaRecorder = new MediaRecorder( + this.mediaStream, {mimeType: "video/webm; codecs=vp9"} + ); + this.screenStream = screenStream; + this.mediaRecorder.ondataavailable=(event)=>{ + this.videoResponse(event); + } + this.mediaRecorder.start(); + VIDEO.srcObject = this.mediaStream; + VIDEO.muted = true; + } + next(){ + let ended =this.index >= this.questions.length - 1 + if (ended){ + confirm("Do you want to complete your interview?").then(bool=>{ + if (!bool) return; + REFRESHER.disabled = false; + if (this.useVideo){ + say(FINALSAY.value).then(resp=>{ + ["screen", "media"].forEach(type=>{ + this[type+"Stream"].getTracks().forEach(i=>i.stop()); + }) + this.mediaRecorder.stop(); + }); + return; + } + this.textResponse(this.questions, this.answers); + }); + return; + } + if (this.useVideo){ + // maybe check if speechrecog text is plenty enough + } else if (ANSWERBOX.value.trim() == ""){ + return alert("Enter a response to proceed!"); + } else{ + this.blur(); + } + this.ask(++this.index); + } + previous(){ + if (this.index <= 0){ + return alert("Err... this is the first question..."); + } + if (this.useVideo){ + // maybe check if speechrecog text is plenty enough + } else{ + this.blur(); + } + this.ask(--this.index); + } + pause(){ + speechSynthesis.pause(); + VIDEO.pause(); + this.mediaRecorder.pause(); + // pause audio recorder (screen recording) + } + resume(){ + // resume audio recorder (screen recording) + this.mediaRecorder.resume(); + VIDEO.play(); + speechSynthesis.resume(); + if (!speechSynthesis.speaking && !speechSynthesis.paused){ + this.disenable(false); + } + } + blur(){ + this.questionBox.setAttribute("disabled", "true"); + setTimeout(()=>this.questionBox.removeAttribute("disabled"), 200) + } + exit(message="Sorry, an error occured. You will have to restart."){ + alert(message, ["RESTART"]).then(()=>location.reload()); + } + textResponse(){ + switchScreen("TXTRESP"); + for (let index in questions){ // proper way to loop??? :) + let row = add(make("tr"), TEXTRESPONSE); + index = +index; + ["questions", "answers"].forEach( + arg=>add(make("td"), row).textContent=this[arg][index] + ) + } + } + videoResponse(event){ + showLoading("making video..."); + switchScreen("VIDRESP"); + this.videoFile = event.data; + VIDEORESPONSE.src = URL.createObjectURL(this.videoFile); + (SAVE.downloader = make('a')).download = "ai-interview.webm"; + SAVE.downloader.href = VIDEORESPONSE.src; + SAVE.onclick = ()=>{SAVE.downloader.click()}; + hideLoading(); + } + static containers = [TEXTINTERVIEW, VIDEOINTERVIEW]; + // in SETUP, do a starter. +} + +// set some things for CONTROLS +Object.defineProperty(CONTROLS, "layout", { + get: function(){return CONTROLS.classList.contains("side")}, + set: function(value){ + let property = value?"add":"remove"; + CONTROLS.classList[property]("side"); + CONTROLS.parentElement.classList[property]("side"); + } +}) +function layout(){ + CONTROLS +} \ No newline at end of file diff --git a/script.js b/script.js deleted file mode 100644 index 6723446..0000000 --- a/script.js +++ /dev/null @@ -1,57 +0,0 @@ -identify() -function makeQuestion(question, index) { - let questionHolder = make('li'); - [['span', 'questionnumber', index], ['span', 'question', question], - ['button', 'removebutton', 'X'], ['div', 'upanddown']].forEach(e=>{ - questionHolder.appendChild((questionHolder[e[1]]=make(e[0]))).className=e[1]; - questionHolder[e[1]].textContent=e[2]; - //appendChild would return the element so you can add a className - }); - [['up', '/\\'], ['down', '\\/']].forEach((a)=>{ - questionHolder.upanddown - .appendChild(questionHolder[a[0]]=make('button')).className=a[0]; - questionHolder[a[0]].textContent=a[1] - }); - questionHolder.question.contentEditable=true;//make contentEditable - questionslist.append(questionHolder);//add to DOM - //start filling in - questionHolder.questionnumber.textContent=index; - questionHolder.question.value=question; - //events - questionHolder.removebutton.onclick=()=>questionHolder.remove();//for deleting the question - questionHolder.upanddown.onclick=()=>{ - if (event.target.parentElement!=questionHolder.upanddown)return;//so that I can use ternary ifelse - let dir = event.target.className; - (event.target.className=='up') - ?(questionslist.firstElementChild!=questionHolder) - &&questionslist - .insertBefore(questionHolder, questionHolder.previousElementSibling) - :(questionslist.lastElementChild!=questionHolder) - &&((questionslist.lastElementChild - .previousElementSibling==questionHolder) - ?questionslist.appendChild(questionHolder) - :questionslist.insertBefore(questionHolder - , questionHolder.nextElementSibling.nextElementSibling) - ) - }; - questionHolder.onclick=()=>questionslist.querySelectorAll('.questionnumber').forEach((n, i)=>n.textContent=i+1); - questionHolder.question.focus() -} -(function loader(){ - window.loading = make(); loading.id = 'loading'; - for (let n = 0; n<8; n++){ - let c = make(); c.id = 'c'+n; - c.style.animationDelay = n/8+'s'; -// use other numbers appart from .125 to see effects; - loading.appendChild(c); loading.hidden = 1; - } - document.body.appendChild(loading); -})(); -Object.defineProperty(loading, 'loading', { - set: function (b) { - eval(b)?document.body.appendChild(loading):loading.remove(); - }, - get: function () { - return loading.isConnected; - } -}) \ No newline at end of file diff --git a/scripts.js b/scripts.js new file mode 100644 index 0000000..b32d129 --- /dev/null +++ b/scripts.js @@ -0,0 +1,98 @@ +// initialize the loading element, that blocks the screen and all +function loader(){ + let loading; + (loading = make()).id = "LOADING"; + showLoading = (text="") => add(loading).style.setProperty("--content", `"${text}"`); + hideLoading = () => loading.remove(); + isPageLoading = () => loading.isConnected; + const N = 10 + for (let n = 0; n < N; n++){ + let c; + (c = make()).id = 'c'+n; + c.style.animationDelay = n/N+'s'; + // use other numbers appart from .1 to see effects; + // will have to change animation-duration and dimensions too + loading.append(c); + } +}; + + +// create switch inside SHOULDVIDEO +function createVideoToggle(){ + videoSwitch = new Switch(SHOULDVIDEO + , VIDEOSETTINGS.style, "display", ["none", ""]) +} + +// create search ui for voices +function createVoiceSearchUI(){ + speechSearch = new SearchUI(VOICETHINGS + , ()=>speechSynthesis.getVoices().map(i=>i.name) + // , ()=>["Daniel", "Ogirimah", "Enesi"] + ) +} + +// for "data persistence"... +function saveData(){ + // questions + localStorage.questions = JSON.stringify([...QUESTIONLIST.children].map(q=>q.obj.question)); + // FIRST|FINAL-->SAY + ["FIRSTSAY", "FINALSAY"].forEach(pos=>{ + if (window[pos]){ + localStorage[pos] = window[pos].value; + } + }) +} +function restoreSavedData(){ + const QUESTIONS = localStorage.questions?JSON.parse(localStorage.questions):[]; + QUESTIONS.forEach(q=>(new Question(q))); + ["FIRSTSAY", "FINALSAY"].forEach(pos=>{ + if (localStorage[pos]){ + window[pos].value = localStorage[pos]; + } + }) +} + +// say function to speak +function say(text="", voice){ + // global variable talk + speechSynthesis.cancel(); + TALK.text = text; + if (voice){ + TALK.voice = voice; + } + speechSynthesis.speak(TALK); + return new Promise((resolve)=>say.resolve=resolve); +} + + +// get voice from voice name +function getVoice(name){ + return speechSynthesis.getVoices().find(i=>i.name == name); +} + +// get user consent for recording screen and making video (remove) +function consentToRecording(){ + confirm(` + To use this app's video feature, you must share your entire screen +
    and allow the app to record system audio
    + The app does not store recorded data after usage.
    + Your privacy is paramount + `, ["decline", "agree"]).then(bool=>{ + // create a toggle to switch video options + createVideoToggle(); + if (bool){ + // create voice search ui + createVoiceSearchUI() + if (localStorage.voice){ + speechSearch.value = localStorage.voice; + } + + // initialize SpeechSynthesisUtterance + TALK = new SpeechSynthesisUtterance(); + TALK.onend=()=>say.resolve(true); + } else{ + videoSwitch.switch.click(); + SHOULDVIDEO.remove(); + } + }) +} \ No newline at end of file diff --git a/setup.js b/setup.js deleted file mode 100644 index 43e3e3f..0000000 --- a/setup.js +++ /dev/null @@ -1,29 +0,0 @@ -//start from the top -// first get the saved questions if any -let baseli = getS('li'), //the base for adding questions -questions = localStorage.questions && JSON.parse( - localStorage.questions), currentQueston - , index = 0, answers = [], userMedia - , recorder, stream, voices = speechSynthesis.getVoices() - , voice, question, downloader = make('a'); -downloader.download = 'interview.mp4'; -if (questions) { - questions.forEach((q,i)=>{ - makeQuestion(q, 1 + i) - }) -} -function mov() { - - p.then(mediaStream=>{ - m = MediaRecorder; - video.onloadedmetadata = (x)=>{ - confirm('play?') && video.play(); - a.push(x) - } - MediaRecorder.start(); - }); - - p.catch(err=>console.log(err)) -} -// startbutton.onclick = mov -loading.loading = 0; diff --git a/styles.css b/styles.css index e4547a3..11cd669 100644 --- a/styles.css +++ b/styles.css @@ -39,7 +39,7 @@ li, label{ margin-bottom: 10px; display: flex; gap: 10px; padding: 10px 15px; - padding-right: ; + /* padding-right: ; */ background: aliceblue; align-items: center; justify-content: space-evenly; @@ -50,14 +50,14 @@ li, label{ border: 1px solid blue; } #shouldusevideo{scale: 3} -#setup #ondout{margin: ;padding: 10px 5px} +/* #setup #ondout{margin: padding: 10px 5px} */ #voicechoices>label{ align-self: end; } #voicechoices input{ scale: 1.5; border: 1px solid blue; } -#voiceschoices h4{align-self: } +/* #voiceschoices h4{align-self: } */ button{ background: #0078d4; color: white; border-color: aliceblue; @@ -153,40 +153,4 @@ th{color: darkblue; text-align: -webkit-auto;} button{font-size: 1em; height: fit-content} } - /* loading */ -#loading, #loading:before{ - width: 80px; - height: 80px; - border: none; - position: fixed; -/* display: none; */ - top: calc(50vh - 40px); - left: calc(50vw - 40px); z-index: 199; - margin: 0; -} -#loading:before{ - content: ''; - width: 1000%; height: 1000%; - top: 0; left: 0; border-radius: 0; - background: rgb(0 0 0 / 60%); -} -body #loading>div{ - width: 20px; height: 20px; border: none; background: white; - top: 0; left: calc(50% - 10px);var(--x); position: absolute; -animation: blink 1s cubic-bezier(0, 0, 1, -0.12) infinite; - opacity: 0; z-index: 200; border-radius: 40px; - padding: 0; margin: 0; -} -@-webkit-keyframes blink{ - 0%{opacity: 1} - 50%{opacity: 0} -} -#c0{transform: translate(0px, 0px);} -#c1{transform: translate(20px, 11px);} -#c2{transform: translate(30px, 30px)} -#c3{transform: translate(20px, 50px)} -#c4{transform: translate(0px, 60px)} -#c5{transform: translate(-20px, 50px)} -#c6{transform: translate(-30px, 30px)} -#c7{transform: translate(-20px, 11px);} -/* end loading */ \ No newline at end of file + \ No newline at end of file diff --git a/web.config b/web.config deleted file mode 100644 index 6c8e9b0..0000000 --- a/web.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/webint.css b/webint.css new file mode 100644 index 0000000..4582dce --- /dev/null +++ b/webint.css @@ -0,0 +1,537 @@ +/* General */ +*{ + transition: 0.1s; + clear: both; + /* VARIABLES */ + --color: lightblue; + --b: 1px solid var(--color); + --usevideo: flex; + /* end VARIABLES */ +} + +body{ + font: 1.4rem math; + display: flex; + flex-flow: column; + align-items: center; + gap: 10px; + margin: 30px; +} + +[disabled]{ + filter: blur(1px) grayscale(.5); +} + +/* buttons */ +button, #SAY{ + color: var(--color); + background: transparent; + font-size: 0.8em; + border: thin lightblue; + padding: 5px; + cursor: pointer; + transition: .2s; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + text-transform: uppercase; + /* box-sizing: content-box; */ +} + +button:hover, button:active{ + padding: 10px; + border: 1px solid lightblue; + border-radius: 0.8em; +} +button:active{ + background-color: var(--color); + color: white; +} + +/* icon buttons */ +.icon{ + width: 2em; + height: 2em; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + +} +.icon:hover, .icon:focus, .icon:active{ + padding: 0; + border-radius: 1em; +} +/* end icon buttons */ + +/* major buttons */ +.major{ + background: var(--color); + color: white; + font-size: 1em; + border-radius: 0.5em; + padding: 10px 20px; + margin: 5px; + text-align: center; + display: block; + justify-self: center; + margin-top: 20px; +} +.major:hover, .major:active{ + padding: 10px 20px; + background-color: blue; + border-radius: 0.5em; +} +.major:active{ + background-color: rgb(0, 0, 127); +} +/* end major buttons */ + +/* end buttons */ + +/* input text */ +[type="text"], textarea{ + padding: 7px 0; + border: none; + border-bottom: var(--b); + border-width: 2px; + width: fit-content; + color: gray; + font-size: 0.9em; +} +[type="text"]:hover, textarea:hover{ + border-color: blue +} +[type="text"]:active, [type="text"]:focus, textarea:active, textarea:focus{ + border-color: darkblue; + outline: none; +} +/* end input text */ + +/* More specific general */ +#REFRESHER{ + position: absolute; + float: right; + align-self: flex-end; +} + +h2{ + border-bottom: var(--b); + margin: 0; + padding: 5px; +} + +#ERRORS{ + color: darkred; + font-size: 0.9em; + margin: 0; + padding: 0; + letter-spacing: 0.1em; +} +#ACCESSORIES{ + display: none; +} + +/* LOADING */ +#LOADING, #LOADING:before{ + width: 80px; + height: 80px; + border: none; + position: fixed; + top: calc(50vh - 40px); + left: calc(50vw - 40px); z-index: 20; + margin: 0; +} +#LOADING:before{ + content: var(--content); + font-size: 0.7em; + /* display: flex; */ + text-align: center; + padding-top: 50%; + color: white; + width: 100%; + height: 100%; + top: 0; + left: 0; + border-radius: 0; + background: rgb(0 0 0 / 60%); +} +#LOADING>div{ + width: 16px; + height: 16px; + border-radius: 32px; + padding: 0; + margin: 0; + border: none; + background: white; + position: absolute; + top: 0; + left: calc(50% - 10px); + opacity: 0; + z-index: 21; + animation: blink 0.8s cubic-bezier(0, 0, 1, -0.12) infinite; +} +@keyframes blink{ + 0%{opacity: 1} + 50%{opacity: 0} +} +#c0{transform: translate(0px, 0px);} +#c1{transform: translate(20px, 11px);} +#c2{transform: translate(30px, 30px)} +#c3{transform: translate(20px, 50px)} +#c4{transform: translate(0px, 60px)} +#c5{transform: translate(-20px, 50px)} +#c6{transform: translate(-30px, 30px)} +#c7{transform: translate(-20px, 11px);} +/* end LOADING */ + +/* INTERVIEW CONTROLS */ +#CONTROLS{ + display: flex; + flex-flow: row; + justify-content: space-between; + gap: 5px; + align-items: center; +} +#CONTROLS.side{ + flex-flow: column; + align-items: end; + grid-area: ctrl; +} +#CONTROLS>button, #SAY{ + border-right: var(--b); + gap: 0px +} +#CONTROLS.side>button, #CONTROLS.side>div:not(.switchold){ + border: none; + border-bottom: var(--b); +} +#SAY>*{ + gap: 0px; + margin: 0; + font-size: 1em; +} +#CONTROLS select{ + padding: 2px 15px 2px 10px; + background-color: var(--color); + color: white; + border: none; + border-radius: 0.2em; + /* margin-left: 0.5em; */ + width: 1em; + cursor: pointer; + /* transform: scaleX(-1); */ + /* width: 1em; */ + /* display: none; */ +} +#CONTROLS select:active{ + background-color: blue; +} +select:active, select:focus{ + outline: none; +} +#CONTROLS option{ + text-transform: uppercase; + border-radius: 1em; + padding: 51px; +} +#CONTROLS option:hover{ + background-color: red; +} +/* end INTERVIEW CONTROLS */ + +/* Modals */ +.modal{ + border: var(--b); + border-width: 3px; + border-radius: 0.8em; + /* display: none; */ + flex-flow: row; + width: 18em; + position: fixed; + /* top: 0; */ + left: calc(50% - 9em); + flex-wrap: wrap; + z-index: 23; + background-color: white; + padding: 10px; + box-sizing: border-box; + transition: 0.3s; + transform: scale(0); + align-items: center; + justify-content: space-evenly; +} +.modal.shown{ + display: flex; + transform: scale(1); +} +.modal.shown.blink{ + border-color: red; +} +.modal>p{ + width: -webkit-fill-available; + text-align: center; + border: var(--b); + border-radius: 0.3em; + padding: 5px; + margin: 0; + max-height: 5em; + overflow-y: scroll; +} +/* end Modals */ + +/* end More specific general */ + + + + +/* end General */ + +/* SETUP SCREEN */ +body>div{ + display: flex; + flex-flow: column; + justify-content: center; + gap: 10px; + /* border: 1px solid black; */ +} +#SETUPDESC{ + display: flex; + flex-flow: row; + justify-content: space-between; + align-items: center; + border-bottom: var(--b); +} +/* #SETUPDESC>span{ + border-bottom: var(--b); +} */ +#SETUPDESC>button{ + font-size: 1em; +} +#QUESTIONLIST{ + display: flex; + flex-flow: column; + gap: 5px; + list-style-type: decimal; +} + +#QUESTIONLIST>li{ + /* display: flex; */ + flex-flow: row; + /* gap: 5px; */ + align-items: center; + position: relative; + flex-wrap: nowrap; + max-width: 100%; + /* border-bottom: var(--b); */ +} +#QUESTIONLIST>li.highlight{ + border: var(--b); + border-radius: 1em; +} +#QUESTIONLIST>li [type="text"]{ + /* justify-self: flex-start; */ + display: inline-block; + width: 70%; +} +*{ + --x: #QUESTIONLIST>li span; +} +--x{ + display: none; + color: red; +} +#QUESTIONLIST>li button{ + float: right; clear: left; + outline-color: red; + /* display: none; */ +} +#SHOULDVIDEO{ + display: flex; + gap: 10px; + align-items: center; +} +.switchold{ + width: 2em; + height: 1em; + display: flex; + flex-flow: row; + justify-content: end; + border-radius: 40px; + border: var(--b); + border-width: 3px; + align-items: center; + padding: 2px; +} +.switchold.off{ + justify-content: start; + background-color: black; +} +.switch{ + background-color: lightblue; + width: 1em; + height: 1em; + border-radius: 1em; + cursor: pointer; +} +.switch:hover{ + filter: drop-shadow(2px 4px 6px black); +} + +#VIDEOSETTINGS{ + display: flex; + flex-flow: column; + gap: 10px; + border-top: var(--b); + border-width: 3px; + margin-top: 20px; + padding-top: 10px; +} +#VIDEOSETTINGS>ul{ + list-style-type: none; +} +#VOICETHINGS{ + display: flex; + gap: 20px; + flex-flow: row; + align-items: center; +} +.searchui{ + display: flex; + flex-flow: column; + /* justify-content: baseline; */ +} +.searchui>ul{ + width: 50%; + font-size: 0.9em; + position: absolute; + margin-top: 2em; + z-index: 10; + padding: 10px; + border: var(--b); + border-radius: 0.5em; + overflow-y: scroll; + background-color: white; + height: 10em; +} +.searchui li{ + cursor: pointer; + padding: 3px; +} +.searchui li:hover{ + padding: 5px; + border: var(--b); + border-radius: 0.5em; +} +/* end SETUP SCREEN */ + +/* VIDEOINTERVIEW SCREEN */ +#VIDEO{ + border-radius: 0.5em; + border: var(--b); + border-width: 10px; + width: 20em; + transform: scaleX(-1); +} +#CC{ + font-size: 0.75em; + border-bottom: var(--b); + border-width: 3px; + padding: 0 0 5px 0; + letter-spacing: 1px; +} + +/* end VIDEOINTERVIEW SCREEN */ +[id*="INTERVIEW"].side{ + display: grid; + grid-template-areas: + "qBox ctrl" + "tBox ctrl"; + grid-template-columns: 22em 10em; + justify-content: center; + align-items: center; + align-content: center; + justify-items: stretch; +} +/* TEXTINTERVIEW SCREEN */ + +#TEXTINTERVIEW{ + font-size: 0.9em; + align-items: center; +} +#QUESTIONBOX{ + width: 20em; + overflow-y: scroll; + border-radius: 0.8em; + padding: 20px; + scrollbar-width: none; + border: var(--b); + max-height: 50vh; + +} +#ANSWERBOX{ + background-color: white; + font-size: 0.9em; + resize: none; + margin-top: 5px; + padding: 0 10px +} + +/* end TEXTINTERVIEW SCREEN */ + + + +/* VIDEORESPONSE SCREEN */ +/* end VIDEORESPONSE SCREEN */ + + +/* TEXTRESPONSE SCREEN */ +#TEXTRESPONSE{ + border-collapse: collapse; +} +th{ + color: var(--color); +} +tr{ + gap: 0; + border-top: var(--b); + width: min-content; +} +td{ + padding: 10px; + border-left: var(--b); + width: fit-content; +} +td:nth-child(1){ + text-align: right; + border-left: none; + border-right: var(--b); +} +@media print { + #REFRESHER, #PRINT{ + display: none; + } + h2{ + border: none; + } + /* style */ +} +/* end TEXTRESPONSE SCREEN */ + + + + + +/* +Do a 'share interview' feature. +save all questions to a json, +save voice +save voice settings + +or save in queries that can be read (will be too long) + +or reroute by bitly + + +*/ \ No newline at end of file